mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-11 18:29:27 +00:00
Compare commits
374 Commits
reduce-mot
...
theme-by-u
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f3f0dfc62b | ||
![]() |
d4125d433f | ||
![]() |
c824e58e0a | ||
![]() |
c6617718b7 | ||
![]() |
cd1ca72e45 | ||
![]() |
4f9ca3b173 | ||
![]() |
1349c8520c | ||
![]() |
6d1a55cc3a | ||
![]() |
23a9ae6835 | ||
![]() |
dbd1e928de | ||
![]() |
e86ad21ce2 | ||
![]() |
0d97afb3f2 | ||
![]() |
03a415beff | ||
![]() |
44cc75afbc | ||
![]() |
748642a8d6 | ||
![]() |
3d5c65d652 | ||
![]() |
a26bf80b13 | ||
![]() |
497c6c35f1 | ||
![]() |
b0b06a2787 | ||
![]() |
f3d55447ca | ||
![]() |
1b3d4b77d3 | ||
![]() |
6ec4041c4c | ||
![]() |
d919e8d333 | ||
![]() |
af7bb85667 | ||
![]() |
9061e2039b | ||
![]() |
906e6f4a88 | ||
![]() |
73fbe9a69d | ||
![]() |
2a0f69a629 | ||
![]() |
9411a77f14 | ||
![]() |
de3bf2e088 | ||
![]() |
16181b48ae | ||
![]() |
8682debe61 | ||
![]() |
bdbc9bc1b4 | ||
![]() |
79b9f8d083 | ||
![]() |
3918194d2d | ||
![]() |
e9fef1f873 | ||
![]() |
35face602b | ||
![]() |
803ac496f6 | ||
![]() |
f1173dd84b | ||
![]() |
44dcca9923 | ||
![]() |
bd74d39dd8 | ||
![]() |
172d6c3079 | ||
![]() |
56539e8065 | ||
![]() |
8f6867f142 | ||
![]() |
d51f8995dd | ||
![]() |
f2e35dc70a | ||
![]() |
6487b9b7ea | ||
![]() |
e50b658db7 | ||
![]() |
6efe237639 | ||
![]() |
4a94cfc05b | ||
![]() |
7cbdb1dcfd | ||
![]() |
553bb61db7 | ||
![]() |
786ff787d1 | ||
![]() |
28b3f2970a | ||
![]() |
7d170a710e | ||
![]() |
cc40b50675 | ||
![]() |
b6eaff46e9 | ||
![]() |
674bb0d16a | ||
![]() |
6ff018afc9 | ||
![]() |
ad48732bb7 | ||
![]() |
fef162346a | ||
![]() |
72d208d1ac | ||
![]() |
5a8b1b0fd4 | ||
![]() |
4cfc651799 | ||
![]() |
b4a3f4cb2c | ||
![]() |
f0507a88a6 | ||
![]() |
fe041e442d | ||
![]() |
e5fea98460 | ||
![]() |
31180e3a9e | ||
![]() |
ce0f02a45b | ||
![]() |
53f090356e | ||
![]() |
776c4da688 | ||
![]() |
849922f7be | ||
![]() |
a26701808f | ||
![]() |
904ee2e418 | ||
![]() |
11ae3a77e8 | ||
![]() |
3a12019b64 | ||
![]() |
6c2cf1ff60 | ||
![]() |
02ae0b5864 | ||
![]() |
85fe2213c1 | ||
![]() |
7dbc78f1d6 | ||
![]() |
f965a3504f | ||
![]() |
077f5efe7e | ||
![]() |
ef3bea71a0 | ||
![]() |
fcf655b0ec | ||
![]() |
b263b74916 | ||
![]() |
0f4b6b423a | ||
![]() |
72df585c5e | ||
![]() |
4698a63642 | ||
![]() |
6eb43a7d61 | ||
![]() |
af35b15400 | ||
![]() |
0d50d2664f | ||
![]() |
ff1159402e | ||
![]() |
f8742ae690 | ||
![]() |
c786d26542 | ||
![]() |
3f8ff94002 | ||
![]() |
64a968543b | ||
![]() |
aea98f702b | ||
![]() |
863ff622be | ||
![]() |
730cea6646 | ||
![]() |
7d1f8d618a | ||
![]() |
67b970fcaa | ||
![]() |
38bcdaa6f6 | ||
![]() |
8f1389de66 | ||
![]() |
37ac796c8f | ||
![]() |
716cd19d41 | ||
![]() |
173725f011 | ||
![]() |
ad561b885b | ||
![]() |
d77bdf4ac6 | ||
![]() |
ac3796ec31 | ||
![]() |
8c3fdfb6fb | ||
![]() |
b7c7d0b4b5 | ||
![]() |
8b0e6eed3a | ||
![]() |
603f884e8c | ||
![]() |
97dfccf4c7 | ||
![]() |
fd1e31c0cc | ||
![]() |
1de740e7b5 | ||
![]() |
5abfb90b16 | ||
![]() |
6b691063a8 | ||
![]() |
d1d746e7e6 | ||
![]() |
2fcb64d4a1 | ||
![]() |
3769f8c7c0 | ||
![]() |
f0a56e75f5 | ||
![]() |
15f33e1f19 | ||
![]() |
181122177b | ||
![]() |
684cd0f627 | ||
![]() |
277202e363 | ||
![]() |
b388d1fd42 | ||
![]() |
251e6399f5 | ||
![]() |
f44c5d7a63 | ||
![]() |
cae1ca52f0 | ||
![]() |
f8de2c64a5 | ||
![]() |
34ef5be720 | ||
![]() |
1402802031 | ||
![]() |
816989ab4d | ||
![]() |
d4497ca39c | ||
![]() |
6e39242ca3 | ||
![]() |
0197e32783 | ||
![]() |
87dfed4beb | ||
![]() |
dae991dc89 | ||
![]() |
6197e3483b | ||
![]() |
b2a6c8bd36 | ||
![]() |
938855e13c | ||
![]() |
a8712e3b8e | ||
![]() |
b15b577057 | ||
![]() |
653aeae3d8 | ||
![]() |
0aea6141ad | ||
![]() |
5243c1d871 | ||
![]() |
9449f5ad0a | ||
![]() |
c337bc5f97 | ||
![]() |
6aab60cf45 | ||
![]() |
52e9bc3213 | ||
![]() |
e48b2383cf | ||
![]() |
002a249777 | ||
![]() |
10498ce18d | ||
![]() |
6a5936b2b2 | ||
![]() |
dc68aaa803 | ||
![]() |
e7931ce049 | ||
![]() |
59b2582fe3 | ||
![]() |
8577b0721c | ||
![]() |
91319be855 | ||
![]() |
0dff538298 | ||
![]() |
6ac6d9c6eb | ||
![]() |
6ba0071296 | ||
![]() |
fef5dc4232 | ||
![]() |
ce58962dbb | ||
![]() |
9fb1e1d2ed | ||
![]() |
a29544c1e6 | ||
![]() |
b2b71edd04 | ||
![]() |
028472fc7b | ||
![]() |
b056ce228b | ||
![]() |
0cd4256c0e | ||
![]() |
e274c5b23f | ||
![]() |
ea57846465 | ||
![]() |
3f2e2bc659 | ||
![]() |
e3f2f66206 | ||
![]() |
b95e87845f | ||
![]() |
bc49ebc489 | ||
![]() |
c97d0ce68a | ||
![]() |
a0e7d6f1c6 | ||
![]() |
25d2a5ddac | ||
![]() |
a2f2d64f5c | ||
![]() |
fcdcbbda05 | ||
![]() |
4b5c7fc2de | ||
![]() |
dd64d88afe | ||
![]() |
5feffd08ff | ||
![]() |
543c7df3e0 | ||
![]() |
167f859f2a | ||
![]() |
c7d9699a24 | ||
![]() |
a638cf443d | ||
![]() |
438d1c13ef | ||
![]() |
cc8869b9f9 | ||
![]() |
cbdb7406ad | ||
![]() |
f8d2560104 | ||
![]() |
cc48ae82d6 | ||
![]() |
e1bda9b57d | ||
![]() |
89e9316a40 | ||
![]() |
85e4975206 | ||
![]() |
142faba04d | ||
![]() |
c1c6d71ccf | ||
![]() |
5204a565cf | ||
![]() |
05b8a48ba8 | ||
![]() |
41770a89b4 | ||
![]() |
b7d14d7950 | ||
![]() |
4ccef6f28b | ||
![]() |
d1be441455 | ||
![]() |
b3391b34e4 | ||
![]() |
9359e9d475 | ||
![]() |
5453da75ea | ||
![]() |
bc21877008 | ||
![]() |
c9956c65e9 | ||
![]() |
74814cc305 | ||
![]() |
8f231d7b3e | ||
![]() |
5b6818d72d | ||
![]() |
8c75865a02 | ||
![]() |
99d832ac77 | ||
![]() |
240e48f5c1 | ||
![]() |
77fc11cda6 | ||
![]() |
f8f152f118 | ||
![]() |
20a67cf73c | ||
![]() |
dd11b3092e | ||
![]() |
7f3363621e | ||
![]() |
edce1901d7 | ||
![]() |
546087066a | ||
![]() |
a95c589a06 | ||
![]() |
5f6da9d959 | ||
![]() |
bc94ca0a0a | ||
![]() |
dc8d483e8b | ||
![]() |
a329553ce1 | ||
![]() |
1f8cfdd0de | ||
![]() |
fd1f966216 | ||
![]() |
243a2ce0e2 | ||
![]() |
77c1786171 | ||
![]() |
7a7c204d74 | ||
![]() |
dcb74ad2ee | ||
![]() |
fd4c62a852 | ||
![]() |
bf1b0ac949 | ||
![]() |
c6f0c62fd6 | ||
![]() |
ac98672cb7 | ||
![]() |
6b471ba6e7 | ||
![]() |
b28e7d2f06 | ||
![]() |
bf471eb8c3 | ||
![]() |
8c52bc3ffb | ||
![]() |
54b328648a | ||
![]() |
62a9da2de2 | ||
![]() |
25ff96b9bf | ||
![]() |
caeb616dc6 | ||
![]() |
fb79e2cfb2 | ||
![]() |
666316e44a | ||
![]() |
1532093426 | ||
![]() |
2effb0935c | ||
![]() |
06a08bb4f2 | ||
![]() |
09102d34d6 | ||
![]() |
27d683f6e8 | ||
![]() |
b89bd0be3b | ||
![]() |
51e6e6d230 | ||
![]() |
9005f8faa9 | ||
![]() |
7aa2abc9c2 | ||
![]() |
898fd51c7d | ||
![]() |
b3141a0653 | ||
![]() |
1025f73c36 | ||
![]() |
1cd44728df | ||
![]() |
eaab19fb8c | ||
![]() |
c4b2896fac | ||
![]() |
ca2a9f9171 | ||
![]() |
9e868e144d | ||
![]() |
fcb6da55d8 | ||
![]() |
87907b98bd | ||
![]() |
7535d66373 | ||
![]() |
e994e3565d | ||
![]() |
562589a6cb | ||
![]() |
e87f6d6d5e | ||
![]() |
3feedb792a | ||
![]() |
fc290028c6 | ||
![]() |
2f5fd6f0c7 | ||
![]() |
9a3ca59d77 | ||
![]() |
f76c22cb5b | ||
![]() |
aad94624e2 | ||
![]() |
076ab91199 | ||
![]() |
d121b33263 | ||
![]() |
dbfcf310c3 | ||
![]() |
25ea6c2c55 | ||
![]() |
b2b2c21477 | ||
![]() |
09777db549 | ||
![]() |
f108f65e39 | ||
![]() |
2ee9824bcc | ||
![]() |
812461ea00 | ||
![]() |
c430c28c2a | ||
![]() |
d67c463b98 | ||
![]() |
3ffbd435e0 | ||
![]() |
2e2f39adbd | ||
![]() |
5e7f356707 | ||
![]() |
b02d0e58b1 | ||
![]() |
c01d3aee41 | ||
![]() |
6dde7d6945 | ||
![]() |
14c71f436e | ||
![]() |
9acdd9f903 | ||
![]() |
12b2edaa65 | ||
![]() |
d55d388046 | ||
![]() |
ec1dedcb6b | ||
![]() |
d0fbba5063 | ||
![]() |
344e083ac6 | ||
![]() |
0ee6548650 | ||
![]() |
9ff1ce3c96 | ||
![]() |
f53ac94e76 | ||
![]() |
6d084813d5 | ||
![]() |
926f5e3cd8 | ||
![]() |
befc650f81 | ||
![]() |
2019b8992e | ||
![]() |
4d2e9f203f | ||
![]() |
760d898de7 | ||
![]() |
a755af96a6 | ||
![]() |
046b90ae25 | ||
![]() |
a3e8bcf848 | ||
![]() |
4b13dde92e | ||
![]() |
7aa2136c21 | ||
![]() |
47308e7b46 | ||
![]() |
ec1fc09140 | ||
![]() |
e2ad94469a | ||
![]() |
70541ec966 | ||
![]() |
0bf64ee7f4 | ||
![]() |
4ea0c83fbe | ||
![]() |
d126e02747 | ||
![]() |
102a9eeb61 | ||
![]() |
fc3e99e794 | ||
![]() |
70953788cc | ||
![]() |
6c9df587e7 | ||
![]() |
8f58681d83 | ||
![]() |
4a16d9bd44 | ||
![]() |
fcc9da6d85 | ||
![]() |
e03dc2c382 | ||
![]() |
be967940a2 | ||
![]() |
64ad37ed6a | ||
![]() |
01bc45c78b | ||
![]() |
2206644c47 | ||
![]() |
486038c426 | ||
![]() |
711f721007 | ||
![]() |
3b8bc242fe | ||
![]() |
7e80eed003 | ||
![]() |
a7ef498d75 | ||
![]() |
a5de6ff3af | ||
![]() |
806cc2c608 | ||
![]() |
48a160f057 | ||
![]() |
c697843c34 | ||
![]() |
317a2f5b21 | ||
![]() |
220011f15f | ||
![]() |
e8af454705 | ||
![]() |
713b5c7cf7 | ||
![]() |
8b17286fb6 | ||
![]() |
f3705a7e1d | ||
![]() |
d0123b2cce | ||
![]() |
884c22f92b | ||
![]() |
700690474c | ||
![]() |
4686808e53 | ||
![]() |
cf1df712e4 | ||
![]() |
c338e9cb30 | ||
![]() |
8e8fd89d56 | ||
![]() |
f1c360c550 | ||
![]() |
b429ecc376 | ||
![]() |
6d8422513a | ||
![]() |
cb0a48265a | ||
![]() |
c9082724a8 | ||
![]() |
fea83c0873 | ||
![]() |
d3b4014182 | ||
![]() |
5c7fe04562 | ||
![]() |
44e26c925b | ||
![]() |
205dd3f968 | ||
![]() |
86133a0696 | ||
![]() |
2105db9104 | ||
![]() |
c05054e4ff | ||
![]() |
f416b1b5da | ||
![]() |
e7d9032cc4 | ||
![]() |
331385794c | ||
![]() |
a7cacbbbe6 |
@@ -11,6 +11,9 @@
|
||||
"DEV_CONTAINER": "1",
|
||||
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
|
||||
},
|
||||
"remoteEnv": {
|
||||
"NODE_OPTIONS": "--max_old_space_size=8192"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
|
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/release-drafter.yaml
vendored
2
.github/workflows/release-drafter.yaml
vendored
@@ -18,6 +18,6 @@ jobs:
|
||||
pull-requests: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v6.0.0
|
||||
- uses: release-drafter/release-drafter@v6.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
6
.github/workflows/release.yaml
vendored
6
.github/workflows/release.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 90 days stale policy
|
||||
uses: actions/stale@v9.0.0
|
||||
uses: actions/stale@v9.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
|
@@ -65,6 +65,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
config_subentry_id: null,
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
@@ -85,6 +86,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
},
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
config_subentry_id: null,
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
|
@@ -11,6 +11,7 @@ export const mockConfigEntries = (hass: MockHomeAssistant) => {
|
||||
supports_remove_device: false,
|
||||
supports_unload: true,
|
||||
supports_reconfigure: true,
|
||||
supported_subentry_types: {},
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
disabled_by: null,
|
||||
|
@@ -48,6 +48,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "bedroom",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_1"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -71,6 +72,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "backyard",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_2"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -94,6 +96,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: null,
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_3"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -124,6 +127,8 @@ const AREAS: AreaRegistryEntry[] = [
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
temperature_entity_id: null,
|
||||
humidity_entity_id: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
@@ -135,6 +140,8 @@ const AREAS: AreaRegistryEntry[] = [
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
temperature_entity_id: null,
|
||||
humidity_entity_id: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
@@ -146,6 +153,8 @@ const AREAS: AreaRegistryEntry[] = [
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
temperature_entity_id: null,
|
||||
humidity_entity_id: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
|
@@ -47,6 +47,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "bedroom",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_1"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -70,6 +71,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "backyard",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_2"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -93,6 +95,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: null,
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_3"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -123,6 +126,8 @@ const AREAS: AreaRegistryEntry[] = [
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
temperature_entity_id: null,
|
||||
humidity_entity_id: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
@@ -134,6 +139,8 @@ const AREAS: AreaRegistryEntry[] = [
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
temperature_entity_id: null,
|
||||
humidity_entity_id: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
@@ -145,6 +152,8 @@ const AREAS: AreaRegistryEntry[] = [
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
temperature_entity_id: null,
|
||||
humidity_entity_id: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
|
@@ -32,6 +32,8 @@ const createConfigEntry = (
|
||||
supports_remove_device: false,
|
||||
supports_unload: true,
|
||||
supports_reconfigure: true,
|
||||
supported_subentry_types: {},
|
||||
num_subentries: 0,
|
||||
disabled_by: null,
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
@@ -188,6 +190,7 @@ const createEntityRegistryEntries = (
|
||||
): EntityRegistryEntry[] => [
|
||||
{
|
||||
config_entry_id: item.entry_id,
|
||||
config_subentry_id: null,
|
||||
device_id: "mock-device-id",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
@@ -214,6 +217,7 @@ const createDeviceRegistryEntries = (
|
||||
{
|
||||
entry_type: null,
|
||||
config_entries: [item.entry_id],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
manufacturer: "ESPHome",
|
||||
model: "Mock Device",
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import { stripDiacritics } from "../../../src/common/string/strip-diacritics";
|
||||
import type { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import { getStripDiacriticsFn } from "../../../src/util/fuse";
|
||||
|
||||
export function filterAndSort(addons: StoreAddon[], filter: string) {
|
||||
const options: IFuseOptions<StoreAddon> = {
|
||||
@@ -10,8 +8,8 @@ export function filterAndSort(addons: StoreAddon[], filter: string) {
|
||||
isCaseSensitive: false,
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.2,
|
||||
getFn: getStripDiacriticsFn,
|
||||
ignoreDiacritics: true,
|
||||
};
|
||||
const fuse = new Fuse(addons, options);
|
||||
return fuse.search(stripDiacritics(filter)).map((result) => result.item);
|
||||
return fuse.search(filter).map((result) => result.item);
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"backup-uploaded": { backup: HassioBackup };
|
||||
"hassio-backup-uploaded": { backup: HassioBackup };
|
||||
"backup-cleared": undefined;
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export class HassioUploadBackup extends LitElement {
|
||||
this._uploading = true;
|
||||
try {
|
||||
const backup = await uploadBackup(this.hass, file);
|
||||
fireEvent(this, "backup-uploaded", { backup: backup.data });
|
||||
fireEvent(this, "hassio-backup-uploaded", { backup: backup.data });
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: "Upload failed",
|
||||
|
@@ -5,7 +5,6 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { formatDate } from "../../../src/common/datetime/format_date";
|
||||
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
|
||||
import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-textfield";
|
||||
@@ -19,13 +18,10 @@ import type {
|
||||
} from "../../../src/data/hassio/backup";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
|
||||
import type { HomeAssistant, TranslationDict } from "../../../src/types";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import "./supervisor-formfield-label";
|
||||
import type { HaTextField } from "../../../src/components/ha-textfield";
|
||||
|
||||
type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
|
||||
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
|
||||
|
||||
interface CheckboxItem {
|
||||
slug: string;
|
||||
checked: boolean;
|
||||
@@ -67,8 +63,6 @@ const _computeAddons = (addons): AddonCheckboxItem[] =>
|
||||
export class SupervisorBackupContent extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public supervisor?: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public backup?: HassioBackupDetail;
|
||||
@@ -115,10 +109,6 @@ export class SupervisorBackupContent extends LitElement {
|
||||
this._focusTarget?.focus();
|
||||
}
|
||||
|
||||
private _localize = (key: BackupOrRestoreKey) =>
|
||||
this.supervisor?.localize(`backup.${key}`) ||
|
||||
this.localize!(`ui.panel.page-onboarding.restore.${key}`);
|
||||
|
||||
protected render() {
|
||||
if (!this.onboarding && !this.supervisor) {
|
||||
return nothing;
|
||||
@@ -132,8 +122,8 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${this.backup
|
||||
? html`<div class="details">
|
||||
${this.backup.type === "full"
|
||||
? this._localize("full_backup")
|
||||
: this._localize("partial_backup")}
|
||||
? this.supervisor?.localize("backup.full_backup")
|
||||
: this.supervisor?.localize("backup.partial_backup")}
|
||||
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
|
||||
${this.hass
|
||||
? formatDateTime(
|
||||
@@ -145,7 +135,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
</div>`
|
||||
: html`<ha-textfield
|
||||
name="backupName"
|
||||
.label=${this._localize("name")}
|
||||
.label=${this.supervisor?.localize("backup.name")}
|
||||
.value=${this.backupName}
|
||||
@change=${this._handleTextValueChanged}
|
||||
>
|
||||
@@ -153,11 +143,13 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${!this.backup || this.backup.type === "full"
|
||||
? html`<div class="sub-header">
|
||||
${!this.backup
|
||||
? this._localize("type")
|
||||
: this._localize("select_type")}
|
||||
? this.supervisor?.localize("backup.type")
|
||||
: this.supervisor?.localize("backup.select_type")}
|
||||
</div>
|
||||
<div class="backup-types">
|
||||
<ha-formfield .label=${this._localize("full_backup")}>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor?.localize("backup.full_backup")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="full"
|
||||
@@ -166,7 +158,9 @@ export class SupervisorBackupContent extends LitElement {
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield .label=${this._localize("partial_backup")}>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor?.localize("backup.partial_backup")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="partial"
|
||||
@@ -202,7 +196,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this._localize("folders")}
|
||||
.label=${this.supervisor?.localize("backup.folders")}
|
||||
.iconPath=${mdiFolder}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
@@ -222,7 +216,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this._localize("addons")}
|
||||
.label=${this.supervisor?.localize("backup.addons")}
|
||||
.iconPath=${mdiPuzzle}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
@@ -247,7 +241,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${!this.backup
|
||||
? html`<ha-formfield
|
||||
class="password"
|
||||
.label=${this._localize("password_protection")}
|
||||
.label=${this.supervisor?.localize("backup.password_protection")}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.backupHasPassword}
|
||||
@@ -259,7 +253,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${this.backupHasPassword
|
||||
? html`
|
||||
<ha-password-field
|
||||
.label=${this._localize("password")}
|
||||
.label=${this.supervisor?.localize("backup.password")}
|
||||
name="backupPassword"
|
||||
.value=${this.backupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
@@ -267,7 +261,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
</ha-password-field>
|
||||
${!this.backup
|
||||
? html`<ha-password-field
|
||||
.label=${this._localize("confirm_password")}
|
||||
.label=${this.supervisor?.localize("backup.confirm_password")}
|
||||
name="confirmBackupPassword"
|
||||
.value=${this.confirmBackupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
|
@@ -72,7 +72,7 @@ export class DialogHassioBackupUpload
|
||||
</ha-header-bar>
|
||||
</div>
|
||||
<hassio-upload-backup
|
||||
@backup-uploaded=${this._backupUploaded}
|
||||
@hassio-backup-uploaded=${this._backupUploaded}
|
||||
.hass=${this.hass}
|
||||
></hassio-upload-backup>
|
||||
</ha-dialog>
|
||||
|
@@ -35,7 +35,6 @@ import { fileDownload } from "../../../../src/util/file_download";
|
||||
import "../../components/supervisor-backup-content";
|
||||
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
|
||||
import type { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
|
||||
import type { BackupOrRestoreKey } from "../../util/translations";
|
||||
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
|
||||
|
||||
@customElement("dialog-hassio-backup")
|
||||
@@ -43,7 +42,7 @@ class HassioBackupDialog
|
||||
extends LitElement
|
||||
implements HassDialog<HassioBackupDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@@ -62,9 +61,13 @@ class HassioBackupDialog
|
||||
this._dialogParams = dialogParams;
|
||||
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
|
||||
if (!this._backup) {
|
||||
this._error = this._localize("no_backup_found");
|
||||
this._error = this._dialogParams.supervisor?.localize(
|
||||
"backup.no_backup_found"
|
||||
);
|
||||
} else if (this._dialogParams.onboarding && !this._backup.homeassistant) {
|
||||
this._error = this._localize("restore_no_home_assistant");
|
||||
this._error = this._dialogParams.supervisor?.localize(
|
||||
"backup.restore_no_home_assistant"
|
||||
);
|
||||
}
|
||||
this._restoringBackup = false;
|
||||
}
|
||||
@@ -82,13 +85,6 @@ class HassioBackupDialog
|
||||
return true;
|
||||
}
|
||||
|
||||
private _localize(key: BackupOrRestoreKey) {
|
||||
return (
|
||||
this._dialogParams!.supervisor?.localize(`backup.${key}`) ||
|
||||
this._dialogParams!.localize!(`ui.panel.page-onboarding.restore.${key}`)
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._dialogParams || !this._backup) {
|
||||
return nothing;
|
||||
@@ -102,7 +98,7 @@ class HassioBackupDialog
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this._localize("close")}
|
||||
.label=${this._dialogParams.supervisor?.localize("backup.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._restoringBackup}
|
||||
@@ -150,7 +146,6 @@ class HassioBackupDialog
|
||||
.supervisor=${this._dialogParams.supervisor}
|
||||
.backup=${this._backup}
|
||||
.onboarding=${this._dialogParams.onboarding || false}
|
||||
.localize=${this._dialogParams.localize}
|
||||
dialogInitialFocus
|
||||
>
|
||||
</supervisor-backup-content>
|
||||
@@ -161,7 +156,7 @@ class HassioBackupDialog
|
||||
.disabled=${this._restoringBackup || !!this._error}
|
||||
@click=${this._restoreClicked}
|
||||
>
|
||||
${this._localize("restore")}
|
||||
${this._dialogParams.supervisor?.localize("backup.restore")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
@@ -196,18 +191,22 @@ class HassioBackupDialog
|
||||
}
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this._localize(
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_title"
|
||||
: "confirm_restore_partial_backup_title"
|
||||
title: supervisor?.localize(
|
||||
`backup.${
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_title"
|
||||
: "confirm_restore_partial_backup_title"
|
||||
}`
|
||||
),
|
||||
text: this._localize(
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_text"
|
||||
: "confirm_restore_partial_backup_text"
|
||||
text: supervisor?.localize(
|
||||
`backup.${
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_text"
|
||||
: "confirm_restore_partial_backup_text"
|
||||
}`
|
||||
),
|
||||
confirmText: this._localize("restore"),
|
||||
dismissText: this._localize("cancel"),
|
||||
confirmText: supervisor?.localize("backup.restore"),
|
||||
dismissText: supervisor?.localize("backup.cancel"),
|
||||
}))
|
||||
) {
|
||||
this._restoringBackup = false;
|
||||
@@ -227,7 +226,8 @@ class HassioBackupDialog
|
||||
this.closeDialog();
|
||||
} catch (error: any) {
|
||||
this._error =
|
||||
error?.body?.message || this._localize("restore_start_failed");
|
||||
error?.body?.message ||
|
||||
supervisor?.localize("backup.restore_start_failed");
|
||||
} finally {
|
||||
this._restoringBackup = false;
|
||||
}
|
||||
@@ -286,7 +286,7 @@ class HassioBackupDialog
|
||||
title: supervisor.localize("backup.remote_download_title"),
|
||||
text: supervisor.localize("backup.remote_download_text"),
|
||||
confirmText: supervisor.localize("backup.download"),
|
||||
dismissText: this._localize("cancel"),
|
||||
dismissText: supervisor?.localize("backup.cancel"),
|
||||
});
|
||||
if (!confirm) {
|
||||
return;
|
||||
@@ -302,7 +302,7 @@ class HassioBackupDialog
|
||||
private get _computeName() {
|
||||
return this._backup
|
||||
? this._backup.name || this._backup.slug
|
||||
: this._localize("unnamed_backup");
|
||||
: this._dialogParams!.supervisor?.localize("backup.unnamed_backup") || "";
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../src/common/translations/localize";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface HassioBackupDialogParams {
|
||||
@@ -8,7 +7,6 @@ export interface HassioBackupDialogParams {
|
||||
onRestoring?: () => void;
|
||||
onboarding?: boolean;
|
||||
supervisor?: Supervisor;
|
||||
localize?: LocalizeFunc;
|
||||
}
|
||||
|
||||
export const showHassioBackupDialog = (
|
||||
|
@@ -9,6 +9,7 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-alert";
|
||||
@@ -18,7 +19,11 @@ import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-faded";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/components/ha-markdown";
|
||||
import "../../../src/components/ha-md-list";
|
||||
import "../../../src/components/ha-md-list-item";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import "../../../src/components/ha-switch";
|
||||
import type { HaSwitch } from "../../../src/components/ha-switch";
|
||||
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
|
||||
import {
|
||||
fetchHassioAddonChangelog,
|
||||
@@ -121,6 +126,8 @@ class UpdateAvailableCard extends LitElement {
|
||||
|
||||
const changelog = changelogUrl(this._updateType, this._version_latest);
|
||||
|
||||
const createBackupTexts = this._computeCreateBackupTexts();
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
@@ -160,6 +167,30 @@ class UpdateAvailableCard extends LitElement {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
${createBackupTexts
|
||||
? html`
|
||||
<hr />
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${createBackupTexts.title}
|
||||
</span>
|
||||
|
||||
${createBackupTexts.description
|
||||
? html`
|
||||
<span slot="supporting-text">
|
||||
${createBackupTexts.description}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
<ha-switch
|
||||
slot="end"
|
||||
id="create-backup"
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: html`<ha-circular-progress
|
||||
aria-label="Updating"
|
||||
@@ -227,6 +258,48 @@ class UpdateAvailableCard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _computeCreateBackupTexts():
|
||||
| { title: string; description?: string }
|
||||
| undefined {
|
||||
// Addon backup
|
||||
if (
|
||||
this._updateType === "addon" &&
|
||||
atLeastVersion(this.hass.config.version, 2025, 2, 0)
|
||||
) {
|
||||
const version = this._version;
|
||||
return {
|
||||
title: this.supervisor.localize("update_available.create_backup.addon"),
|
||||
description: this.supervisor.localize(
|
||||
"update_available.create_backup.addon_description",
|
||||
{ version: version }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Old behavior
|
||||
if (this._updateType && ["core", "addon"].includes(this._updateType)) {
|
||||
return {
|
||||
title: this.supervisor.localize(
|
||||
"update_available.create_backup.generic"
|
||||
),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get _shouldCreateBackup(): boolean {
|
||||
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
|
||||
return false;
|
||||
}
|
||||
const createBackupSwitch = this.shadowRoot?.getElementById(
|
||||
"create-backup"
|
||||
) as HaSwitch;
|
||||
if (createBackupSwitch) {
|
||||
return createBackupSwitch.checked;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
get _version(): string {
|
||||
return this._updateType
|
||||
? this._updateType === "addon"
|
||||
@@ -341,14 +414,22 @@ class UpdateAvailableCard extends LitElement {
|
||||
}
|
||||
|
||||
private async _update() {
|
||||
if (this._shouldCreateBackup && this.supervisor.info.state === "freeze") {
|
||||
this._error = this.supervisor.localize("backup.backup_already_running");
|
||||
return;
|
||||
}
|
||||
this._error = undefined;
|
||||
this._updating = true;
|
||||
|
||||
try {
|
||||
if (this._updateType === "addon") {
|
||||
await updateHassioAddon(this.hass, this.addonSlug!);
|
||||
await updateHassioAddon(
|
||||
this.hass,
|
||||
this.addonSlug!,
|
||||
this._shouldCreateBackup
|
||||
);
|
||||
} else if (this._updateType === "core") {
|
||||
await updateCore(this.hass);
|
||||
await updateCore(this.hass, this._shouldCreateBackup);
|
||||
} else if (this._updateType === "os") {
|
||||
await updateOS(this.hass);
|
||||
} else if (this._updateType === "supervisor") {
|
||||
@@ -403,6 +484,17 @@ class UpdateAvailableCard extends LitElement {
|
||||
border-bottom: none;
|
||||
margin: 16px 0 0 0;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
padding: 0;
|
||||
margin-bottom: -16px;
|
||||
}
|
||||
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,4 +0,0 @@
|
||||
import type { TranslationDict } from "../../../src/types";
|
||||
|
||||
export type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
|
||||
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
|
55
package.json
55
package.json
@@ -26,14 +26,14 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.26.0",
|
||||
"@babel/runtime": "7.26.7",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.18.4",
|
||||
"@codemirror/commands": "6.8.0",
|
||||
"@codemirror/language": "6.10.8",
|
||||
"@codemirror/legacy-modes": "6.4.2",
|
||||
"@codemirror/search": "6.5.8",
|
||||
"@codemirror/state": "6.5.1",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.36.2",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.17.2",
|
||||
@@ -91,16 +91,14 @@
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.6.2",
|
||||
"@vaadin/vaadin-themable-mixin": "24.6.2",
|
||||
"@vaadin/combo-box": "24.6.4",
|
||||
"@vaadin/vaadin-themable-mixin": "24.6.4",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"barcode-detector": "2.3.1",
|
||||
"chart.js": "4.4.7",
|
||||
"chartjs-plugin-zoom": "2.2.0",
|
||||
"barcode-detector": "3.0.0",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.40.0",
|
||||
@@ -110,14 +108,15 @@
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"element-internals-polyfill": "1.3.12",
|
||||
"fuse.js": "7.0.0",
|
||||
"echarts": "5.6.0",
|
||||
"element-internals-polyfill": "1.3.13",
|
||||
"fuse.js": "7.1.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||
"home-assistant-js-websocket": "9.4.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.7.11",
|
||||
"intl-messageformat": "10.7.14",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -126,7 +125,7 @@
|
||||
"luxon": "3.5.0",
|
||||
"marked": "15.0.6",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.1",
|
||||
"node-vibrant": "4.0.3",
|
||||
"punycode": "2.3.1",
|
||||
"qr-scanner": "1.4.2",
|
||||
"qrcode": "1.5.4",
|
||||
@@ -138,7 +137,7 @@
|
||||
"tinykeys": "3.0.0",
|
||||
"tsparticles-engine": "2.12.0",
|
||||
"tsparticles-preset-links": "2.12.0",
|
||||
"ua-parser-js": "2.0.0",
|
||||
"ua-parser-js": "2.0.1",
|
||||
"vis-data": "7.1.9",
|
||||
"vis-network": "9.1.9",
|
||||
"vue": "2.7.16",
|
||||
@@ -153,22 +152,22 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.26.0",
|
||||
"@babel/core": "7.26.7",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.3",
|
||||
"@babel/plugin-proposal-decorators": "7.25.9",
|
||||
"@babel/plugin-transform-runtime": "7.25.9",
|
||||
"@babel/preset-env": "7.26.0",
|
||||
"@babel/preset-env": "7.26.7",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.17.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.18.2",
|
||||
"@lokalise/node-api": "13.0.0",
|
||||
"@octokit/auth-oauth-device": "7.1.2",
|
||||
"@octokit/plugin-retry": "7.1.3",
|
||||
"@octokit/rest": "21.1.0",
|
||||
"@rsdoctor/rspack-plugin": "0.4.13",
|
||||
"@rspack/cli": "1.1.8",
|
||||
"@rspack/core": "1.1.8",
|
||||
"@rspack/cli": "1.2.2",
|
||||
"@rspack/core": "1.2.2",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.20",
|
||||
"@types/chromecast-caf-receiver": "6.0.21",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/glob": "8.1.0",
|
||||
@@ -184,16 +183,16 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "8.20.0",
|
||||
"@typescript-eslint/parser": "8.20.0",
|
||||
"@vitest/coverage-v8": "2.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "8.23.0",
|
||||
"@typescript-eslint/parser": "8.23.0",
|
||||
"@vitest/coverage-v8": "3.0.5",
|
||||
"babel-loader": "9.2.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.0",
|
||||
"eslint": "9.18.0",
|
||||
"eslint": "9.20.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-config-prettier": "10.0.1",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-lit": "1.15.0",
|
||||
@@ -201,7 +200,7 @@
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-wc": "2.2.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"fs-extra": "11.3.0",
|
||||
"glob": "11.0.1",
|
||||
"gulp": "5.0.0",
|
||||
"gulp-brotli": "3.0.0",
|
||||
@@ -211,7 +210,7 @@
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "26.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "15.3.0",
|
||||
"lint-staged": "15.4.3",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
@@ -220,12 +219,13 @@
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.4.2",
|
||||
"rspack-manifest-plugin": "5.0.3",
|
||||
"serve": "14.2.4",
|
||||
"sinon": "19.0.2",
|
||||
"tar": "7.4.3",
|
||||
"terser-webpack-plugin": "5.3.11",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.7.3",
|
||||
"vitest": "2.1.8",
|
||||
"vitest": "3.0.5",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||
@@ -239,7 +239,8 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "1.6.3",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"globals": "15.14.0"
|
||||
"globals": "15.14.0",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0"
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20241224.0"
|
||||
version = "20250205.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -64,7 +64,7 @@ echo Core is used from ${coreUrl}
|
||||
HASS_URL="$coreUrl" ./script/develop &
|
||||
|
||||
# serve the frontend
|
||||
yarn dlx serve -l $frontendPort ./hass_frontend -s &
|
||||
./node_modules/.bin/serve -p $frontendPort --single --no-port-switching --config ../script/serve-config.json ./hass_frontend &
|
||||
|
||||
# keep the script running while serving
|
||||
wait
|
||||
|
3
script/serve-config.json
Normal file
3
script/serve-config.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"cleanUrls": false
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { theme2hex } from "./convert-color";
|
||||
|
||||
export const COLORS = [
|
||||
@@ -74,3 +75,12 @@ export function getGraphColorByIndex(
|
||||
getColorByIndex(index);
|
||||
return theme2hex(themeColor);
|
||||
}
|
||||
|
||||
export const getAllGraphColors = memoizeOne(
|
||||
(style: CSSStyleDeclaration) =>
|
||||
COLORS.map((_color, index) => getGraphColorByIndex(index, style)),
|
||||
(newArgs: [CSSStyleDeclaration], lastArgs: [CSSStyleDeclaration]) =>
|
||||
// this is not ideal, but we need to memoize the colors
|
||||
newArgs[0].getPropertyValue("--graph-color-1") ===
|
||||
lastArgs[0].getPropertyValue("--graph-color-1")
|
||||
);
|
||||
|
@@ -26,6 +26,20 @@ const formatDateTimeMem = memoizeOne(
|
||||
})
|
||||
);
|
||||
|
||||
export const formatDateTimeWithBrowserDefaults = (dateObj: Date) =>
|
||||
formatDateTimeWithBrowserDefaultsMem().format(dateObj);
|
||||
|
||||
const formatDateTimeWithBrowserDefaultsMem = memoizeOne(
|
||||
() =>
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
);
|
||||
|
||||
// Aug 9, 2021, 8:23 AM
|
||||
export const formatShortDateTimeWithYear = (
|
||||
dateObj: Date,
|
||||
@@ -65,6 +79,18 @@ const formatShortDateTimeMem = memoizeOne(
|
||||
})
|
||||
);
|
||||
|
||||
export const formatShortDateTimeWithConditionalYear = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => {
|
||||
const now = new Date();
|
||||
if (now.getFullYear() === dateObj.getFullYear()) {
|
||||
return formatShortDateTime(dateObj, locale, config);
|
||||
}
|
||||
return formatShortDateTimeWithYear(dateObj, locale, config);
|
||||
};
|
||||
|
||||
// August 9, 2021, 8:23:15 AM
|
||||
export const formatDateTimeWithSeconds = (
|
||||
dateObj: Date,
|
||||
|
@@ -41,7 +41,7 @@ export class HaProgressButton extends LitElement {
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
@@ -117,6 +117,9 @@ export class HaProgressButton extends LitElement {
|
||||
mwc-button.error slot {
|
||||
visibility: hidden;
|
||||
}
|
||||
:host([destructive]) {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
51
src/components/chart/axis-label.ts
Normal file
51
src/components/chart/axis-label.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import {
|
||||
formatDateMonth,
|
||||
formatDateMonthYear,
|
||||
formatDateVeryShort,
|
||||
formatDateWeekdayShort,
|
||||
} from "../../common/datetime/format_date";
|
||||
import {
|
||||
formatTime,
|
||||
formatTimeWithSeconds,
|
||||
} from "../../common/datetime/format_time";
|
||||
|
||||
export function formatTimeLabel(
|
||||
value: number | Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
minutesDifference: number
|
||||
) {
|
||||
const dayDifference = minutesDifference / 60 / 24;
|
||||
const date = new Date(value);
|
||||
if (dayDifference > 88) {
|
||||
return date.getMonth() === 0
|
||||
? `{bold|${formatDateMonthYear(date, locale, config)}}`
|
||||
: formatDateMonth(date, locale, config);
|
||||
}
|
||||
if (dayDifference > 35) {
|
||||
return date.getDate() === 1
|
||||
? `{bold|${formatDateVeryShort(date, locale, config)}}`
|
||||
: formatDateVeryShort(date, locale, config);
|
||||
}
|
||||
if (dayDifference > 7) {
|
||||
const label = formatDateVeryShort(date, locale, config);
|
||||
return date.getDate() === 1 ? `{bold|${label}}` : label;
|
||||
}
|
||||
if (dayDifference > 2) {
|
||||
return formatDateWeekdayShort(date, locale, config);
|
||||
}
|
||||
if (minutesDifference && minutesDifference < 5) {
|
||||
return formatTimeWithSeconds(date, locale, config);
|
||||
}
|
||||
if (
|
||||
date.getHours() === 0 &&
|
||||
date.getMinutes() === 0 &&
|
||||
date.getSeconds() === 0
|
||||
) {
|
||||
// show only date for the beginning of the day
|
||||
return `{bold|${formatDateVeryShort(date, locale, config)}}`;
|
||||
}
|
||||
return formatTime(date, locale, config);
|
||||
}
|
@@ -1,269 +0,0 @@
|
||||
import { _adapters } from "chart.js";
|
||||
import {
|
||||
startOfSecond,
|
||||
startOfMinute,
|
||||
startOfHour,
|
||||
startOfDay,
|
||||
startOfWeek,
|
||||
startOfMonth,
|
||||
startOfQuarter,
|
||||
startOfYear,
|
||||
addMilliseconds,
|
||||
addSeconds,
|
||||
addMinutes,
|
||||
addHours,
|
||||
addDays,
|
||||
addWeeks,
|
||||
addMonths,
|
||||
addQuarters,
|
||||
addYears,
|
||||
differenceInMilliseconds,
|
||||
differenceInSeconds,
|
||||
differenceInMinutes,
|
||||
differenceInHours,
|
||||
differenceInDays,
|
||||
differenceInWeeks,
|
||||
differenceInMonths,
|
||||
differenceInQuarters,
|
||||
differenceInYears,
|
||||
endOfSecond,
|
||||
endOfMinute,
|
||||
endOfHour,
|
||||
endOfDay,
|
||||
endOfWeek,
|
||||
endOfMonth,
|
||||
endOfQuarter,
|
||||
endOfYear,
|
||||
} from "date-fns";
|
||||
import {
|
||||
formatDate,
|
||||
formatDateMonth,
|
||||
formatDateMonthYear,
|
||||
formatDateVeryShort,
|
||||
formatDateWeekdayDay,
|
||||
formatDateYear,
|
||||
} from "../../common/datetime/format_date";
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDateTimeWithSeconds,
|
||||
} from "../../common/datetime/format_date_time";
|
||||
import {
|
||||
formatTime,
|
||||
formatTimeWithSeconds,
|
||||
} from "../../common/datetime/format_time";
|
||||
|
||||
const FORMATS = {
|
||||
datetime: "datetime",
|
||||
datetimeseconds: "datetimeseconds",
|
||||
millisecond: "millisecond",
|
||||
second: "second",
|
||||
minute: "minute",
|
||||
hour: "hour",
|
||||
day: "day",
|
||||
date: "date",
|
||||
weekday: "weekday",
|
||||
week: "week",
|
||||
month: "month",
|
||||
monthyear: "monthyear",
|
||||
quarter: "quarter",
|
||||
year: "year",
|
||||
};
|
||||
|
||||
_adapters._date.override({
|
||||
formats: () => FORMATS,
|
||||
parse: (value: Date | number) => {
|
||||
if (!(value instanceof Date)) {
|
||||
return value;
|
||||
}
|
||||
return value.getTime();
|
||||
},
|
||||
format: function (time, fmt: keyof typeof FORMATS) {
|
||||
switch (fmt) {
|
||||
case "datetime":
|
||||
return formatDateTime(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "datetimeseconds":
|
||||
return formatDateTimeWithSeconds(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "millisecond":
|
||||
return formatTimeWithSeconds(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "second":
|
||||
return formatTimeWithSeconds(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "minute":
|
||||
return formatTime(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "hour":
|
||||
return formatTime(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "weekday":
|
||||
return formatDateWeekdayDay(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "date":
|
||||
return formatDate(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "day":
|
||||
return formatDateVeryShort(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "week":
|
||||
return formatDateVeryShort(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "month":
|
||||
return formatDateMonth(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "monthyear":
|
||||
return formatDateMonthYear(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "quarter":
|
||||
return formatDate(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "year":
|
||||
return formatDateYear(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
},
|
||||
// @ts-ignore
|
||||
add: (time, amount, unit) => {
|
||||
switch (unit) {
|
||||
case "millisecond":
|
||||
return addMilliseconds(time, amount);
|
||||
case "second":
|
||||
return addSeconds(time, amount);
|
||||
case "minute":
|
||||
return addMinutes(time, amount);
|
||||
case "hour":
|
||||
return addHours(time, amount);
|
||||
case "day":
|
||||
return addDays(time, amount);
|
||||
case "week":
|
||||
return addWeeks(time, amount);
|
||||
case "month":
|
||||
return addMonths(time, amount);
|
||||
case "quarter":
|
||||
return addQuarters(time, amount);
|
||||
case "year":
|
||||
return addYears(time, amount);
|
||||
default:
|
||||
return time;
|
||||
}
|
||||
},
|
||||
diff: (max, min, unit) => {
|
||||
switch (unit) {
|
||||
case "millisecond":
|
||||
return differenceInMilliseconds(max, min);
|
||||
case "second":
|
||||
return differenceInSeconds(max, min);
|
||||
case "minute":
|
||||
return differenceInMinutes(max, min);
|
||||
case "hour":
|
||||
return differenceInHours(max, min);
|
||||
case "day":
|
||||
return differenceInDays(max, min);
|
||||
case "week":
|
||||
return differenceInWeeks(max, min);
|
||||
case "month":
|
||||
return differenceInMonths(max, min);
|
||||
case "quarter":
|
||||
return differenceInQuarters(max, min);
|
||||
case "year":
|
||||
return differenceInYears(max, min);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
// @ts-ignore
|
||||
startOf: (time, unit, weekday) => {
|
||||
switch (unit) {
|
||||
case "second":
|
||||
return startOfSecond(time);
|
||||
case "minute":
|
||||
return startOfMinute(time);
|
||||
case "hour":
|
||||
return startOfHour(time);
|
||||
case "day":
|
||||
return startOfDay(time);
|
||||
case "week":
|
||||
return startOfWeek(time);
|
||||
case "isoWeek":
|
||||
return startOfWeek(time, {
|
||||
weekStartsOn: +weekday! as 0 | 1 | 2 | 3 | 4 | 5 | 6,
|
||||
});
|
||||
case "month":
|
||||
return startOfMonth(time);
|
||||
case "quarter":
|
||||
return startOfQuarter(time);
|
||||
case "year":
|
||||
return startOfYear(time);
|
||||
default:
|
||||
return time;
|
||||
}
|
||||
},
|
||||
// @ts-ignore
|
||||
endOf: (time, unit) => {
|
||||
switch (unit) {
|
||||
case "second":
|
||||
return endOfSecond(time);
|
||||
case "minute":
|
||||
return endOfMinute(time);
|
||||
case "hour":
|
||||
return endOfHour(time);
|
||||
case "day":
|
||||
return endOfDay(time);
|
||||
case "week":
|
||||
return endOfWeek(time);
|
||||
case "month":
|
||||
return endOfMonth(time);
|
||||
case "quarter":
|
||||
return endOfQuarter(time);
|
||||
case "year":
|
||||
return endOfYear(time);
|
||||
default:
|
||||
return time;
|
||||
}
|
||||
},
|
||||
});
|
@@ -1,6 +0,0 @@
|
||||
import type { ChartEvent } from "chart.js";
|
||||
|
||||
export const clickIsTouch = (event: ChartEvent): boolean =>
|
||||
!(event.native instanceof MouseEvent) ||
|
||||
(event.native instanceof PointerEvent &&
|
||||
event.native.pointerType !== "mouse");
|
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ import { LitElement, html, css, svg, nothing } from "lit";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { measureTextWidth } from "../../util/text";
|
||||
|
||||
export interface Node {
|
||||
id: string;
|
||||
@@ -68,15 +69,12 @@ export class HaSankeyChart extends LitElement {
|
||||
|
||||
private _statePerPixel = 0;
|
||||
|
||||
private _textMeasureCanvas?: HTMLCanvasElement;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect,
|
||||
});
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._textMeasureCanvas = undefined;
|
||||
}
|
||||
|
||||
willUpdate() {
|
||||
@@ -477,7 +475,7 @@ export class HaSankeyChart extends LitElement {
|
||||
(node) =>
|
||||
NODE_WIDTH +
|
||||
TEXT_PADDING +
|
||||
(node.label ? this._getTextWidth(node.label) : 0)
|
||||
(node.label ? measureTextWidth(node.label, FONT_SIZE) : 0)
|
||||
)
|
||||
)
|
||||
: 0;
|
||||
@@ -492,18 +490,6 @@ export class HaSankeyChart extends LitElement {
|
||||
: fullSize / nodesPerSection.length;
|
||||
}
|
||||
|
||||
private _getTextWidth(text: string): number {
|
||||
if (!this._textMeasureCanvas) {
|
||||
this._textMeasureCanvas = document.createElement("canvas");
|
||||
}
|
||||
const context = this._textMeasureCanvas.getContext("2d");
|
||||
if (!context) return 0;
|
||||
|
||||
// Match the font style from CSS
|
||||
context.font = `${FONT_SIZE}px sans-serif`;
|
||||
return context.measureText(text).width;
|
||||
}
|
||||
|
||||
private _getVerticalLabelFontSize(label: string, labelWidth: number): number {
|
||||
// reduce the label font size so the longest word fits on one line
|
||||
const longestWord = label
|
||||
@@ -513,7 +499,7 @@ export class HaSankeyChart extends LitElement {
|
||||
longest.length > current.length ? longest : current,
|
||||
""
|
||||
);
|
||||
const wordWidth = this._getTextWidth(longestWord);
|
||||
const wordWidth = measureTextWidth(longestWord, FONT_SIZE);
|
||||
return Math.min(FONT_SIZE, (labelWidth / wordWidth) * FONT_SIZE);
|
||||
}
|
||||
|
||||
|
@@ -1,19 +1,26 @@
|
||||
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import type { VisualMapComponentOption } from "echarts/components";
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
import type { YAXisOption } from "echarts/types/dist/shared";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import {
|
||||
formatNumber,
|
||||
numberFormatToLocale,
|
||||
getNumberFormatOptions,
|
||||
} from "../../common/number/format_number";
|
||||
|
||||
import type { LineChartEntity, LineChartState } from "../../data/history";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import { clickIsTouch } from "./click_is_touch";
|
||||
import type { ECOption } from "../../resources/echarts";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import {
|
||||
getNumberFormatOptions,
|
||||
formatNumber,
|
||||
} from "../../common/number/format_number";
|
||||
import { measureTextWidth } from "../../util/text";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
|
||||
const safeParseFloat = (value) => {
|
||||
const parsed = parseFloat(value);
|
||||
@@ -54,15 +61,19 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
|
||||
|
||||
@state() private _chartData?: ChartData<"line">;
|
||||
@property({ type: String }) public height?: string;
|
||||
|
||||
@state() private _chartData: LineSeriesOption[] = [];
|
||||
|
||||
@state() private _entityIds: string[] = [];
|
||||
|
||||
private _datasetToDataIndex: number[] = [];
|
||||
|
||||
@state() private _chartOptions?: ChartOptions;
|
||||
@state() private _chartOptions?: ECOption;
|
||||
|
||||
@state() private _yWidth = 0;
|
||||
private _hiddenStats = new Set<string>();
|
||||
|
||||
@state() private _yWidth = 25;
|
||||
|
||||
private _chartTime: Date = new Date();
|
||||
|
||||
@@ -72,171 +83,109 @@ export class StateHistoryChartLine extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.data=${this._chartData}
|
||||
.options=${this._chartOptions}
|
||||
.paddingYAxis=${this.paddingYAxis - this._yWidth}
|
||||
chart-type="line"
|
||||
.height=${this.height}
|
||||
style=${styleMap({ height: this.height })}
|
||||
external-hidden
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
></ha-chart-base>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTooltip(params: any) {
|
||||
const time = params[0].axisValue;
|
||||
const title =
|
||||
formatDateTimeWithSeconds(
|
||||
new Date(time),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) + "<br>";
|
||||
const datapoints: Record<string, any>[] = [];
|
||||
this._chartData.forEach((dataset, index) => {
|
||||
if (
|
||||
dataset.tooltip?.show === false ||
|
||||
this._hiddenStats.has(dataset.name as string)
|
||||
)
|
||||
return;
|
||||
const param = params.find(
|
||||
(p: Record<string, any>) => p.seriesIndex === index
|
||||
);
|
||||
if (param) {
|
||||
datapoints.push(param);
|
||||
return;
|
||||
}
|
||||
// If the datapoint is not found, we need to find the last datapoint before the current time
|
||||
let lastData;
|
||||
const data = dataset.data || [];
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
const point = data[i];
|
||||
if (point && point[0] <= time && point[1]) {
|
||||
lastData = point;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!lastData) return;
|
||||
datapoints.push({
|
||||
seriesName: dataset.name,
|
||||
seriesIndex: index,
|
||||
value: lastData,
|
||||
// HTML copied from echarts. May change based on options
|
||||
marker: `<span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:${dataset.color};"></span>`,
|
||||
});
|
||||
});
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
|
||||
return (
|
||||
title +
|
||||
datapoints
|
||||
.map((param) => {
|
||||
const entityId = this._entityIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const entry = this.hass.entities[entityId];
|
||||
const stateValue = String(param.value[1]);
|
||||
let value = stateObj
|
||||
? this.hass.formatEntityState(stateObj, stateValue)
|
||||
: `${formatNumber(
|
||||
stateValue,
|
||||
this.hass.locale,
|
||||
getNumberFormatOptions(undefined, entry)
|
||||
)}${unit}`;
|
||||
const dataIndex = this._datasetToDataIndex[param.seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (data.statistics && data.statistics.length > 0) {
|
||||
value += "<br> ";
|
||||
const source =
|
||||
data.states.length === 0 ||
|
||||
param.value[0] < data.states[0].last_changed
|
||||
? `${this.hass.localize(
|
||||
"ui.components.history_charts.source_stats"
|
||||
)}`
|
||||
: `${this.hass.localize(
|
||||
"ui.components.history_charts.source_history"
|
||||
)}`;
|
||||
value += source;
|
||||
}
|
||||
|
||||
if (param.seriesName) {
|
||||
return `${param.marker} ${param.seriesName}: ${value}`;
|
||||
}
|
||||
return `${param.marker} ${value}`;
|
||||
})
|
||||
.join("<br>")
|
||||
);
|
||||
}
|
||||
|
||||
private _datasetHidden(ev: CustomEvent) {
|
||||
this._hiddenStats.add(ev.detail.name);
|
||||
}
|
||||
|
||||
private _datasetUnhidden(ev: CustomEvent) {
|
||||
this._hiddenStats.delete(ev.detail.name);
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
!this.hasUpdated ||
|
||||
changedProps.has("showNames") ||
|
||||
changedProps.has("startTime") ||
|
||||
changedProps.has("endTime") ||
|
||||
changedProps.has("unit") ||
|
||||
changedProps.has("logarithmicScale") ||
|
||||
changedProps.has("minYAxis") ||
|
||||
changedProps.has("maxYAxis") ||
|
||||
changedProps.has("fitYData")
|
||||
) {
|
||||
this._chartOptions = {
|
||||
parsing: false,
|
||||
interaction: {
|
||||
mode: "nearest",
|
||||
axis: "xy",
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "time",
|
||||
adapters: {
|
||||
date: {
|
||||
locale: this.hass.locale,
|
||||
config: this.hass.config,
|
||||
},
|
||||
},
|
||||
min: this.startTime,
|
||||
max: this.endTime,
|
||||
ticks: {
|
||||
maxRotation: 0,
|
||||
sampleSize: 5,
|
||||
autoSkipPadding: 20,
|
||||
major: {
|
||||
enabled: true,
|
||||
},
|
||||
font: (context) =>
|
||||
context.tick && context.tick.major
|
||||
? ({ weight: "bold" } as any)
|
||||
: {},
|
||||
},
|
||||
time: {
|
||||
tooltipFormat: "datetimeseconds",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
suggestedMin: this.fitYData ? this.minYAxis : null,
|
||||
suggestedMax: this.fitYData ? this.maxYAxis : null,
|
||||
min: this.fitYData ? null : this.minYAxis,
|
||||
max: this.fitYData ? null : this.maxYAxis,
|
||||
ticks: {
|
||||
maxTicksLimit: 7,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: this.unit,
|
||||
},
|
||||
afterUpdate: (y) => {
|
||||
if (this._yWidth !== Math.floor(y.width)) {
|
||||
this._yWidth = Math.floor(y.width);
|
||||
fireEvent(this, "y-width-changed", {
|
||||
value: this._yWidth,
|
||||
chartIndex: this.chartIndex,
|
||||
});
|
||||
}
|
||||
},
|
||||
position: computeRTL(this.hass) ? "right" : "left",
|
||||
type: this.logarithmicScale ? "logarithmic" : "linear",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
let label = `${context.dataset.label}: ${formatNumber(
|
||||
context.parsed.y,
|
||||
this.hass.locale,
|
||||
getNumberFormatOptions(
|
||||
undefined,
|
||||
this.hass.entities[this._entityIds[context.datasetIndex]]
|
||||
)
|
||||
)} ${this.unit}`;
|
||||
const dataIndex =
|
||||
this._datasetToDataIndex[context.datasetIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (data.statistics && data.statistics.length > 0) {
|
||||
const source =
|
||||
data.states.length === 0 ||
|
||||
context.parsed.x < data.states[0].last_changed
|
||||
? `\n${this.hass.localize(
|
||||
"ui.components.history_charts.source_stats"
|
||||
)}`
|
||||
: `\n${this.hass.localize(
|
||||
"ui.components.history_charts.source_history"
|
||||
)}`;
|
||||
label += source;
|
||||
}
|
||||
return label;
|
||||
},
|
||||
},
|
||||
},
|
||||
filler: {
|
||||
propagate: true,
|
||||
},
|
||||
legend: {
|
||||
display: this.showNames,
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0.1,
|
||||
borderWidth: 1.5,
|
||||
},
|
||||
point: {
|
||||
hitRadius: 50,
|
||||
},
|
||||
},
|
||||
segment: {
|
||||
borderColor: (context) => {
|
||||
// render stat data with a slightly transparent line
|
||||
const dataIndex = this._datasetToDataIndex[context.datasetIndex];
|
||||
const data = this.data[dataIndex];
|
||||
return data.statistics &&
|
||||
data.statistics.length > 0 &&
|
||||
(data.states.length === 0 ||
|
||||
context.p0.parsed.x < data.states[0].last_changed)
|
||||
? this._chartData!.datasets[dataIndex].borderColor + "7F"
|
||||
: undefined;
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
onClick: (e: any) => {
|
||||
if (!this.clickForMoreInfo || clickIsTouch(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chart = e.chart;
|
||||
|
||||
const points = chart.getElementsAtEventForMode(
|
||||
e,
|
||||
"nearest",
|
||||
{ intersect: true },
|
||||
true
|
||||
);
|
||||
|
||||
if (points.length) {
|
||||
const firstPoint = points[0];
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: this._entityIds[firstPoint.datasetIndex],
|
||||
});
|
||||
chart.canvas.dispatchEvent(new Event("mouseout")); // to hide tooltip
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
if (
|
||||
changedProps.has("data") ||
|
||||
changedProps.has("startTime") ||
|
||||
@@ -248,13 +197,130 @@ export class StateHistoryChartLine extends LitElement {
|
||||
// so the X axis grows even if there is no new data
|
||||
this._generateData();
|
||||
}
|
||||
|
||||
if (
|
||||
!this.hasUpdated ||
|
||||
changedProps.has("showNames") ||
|
||||
changedProps.has("startTime") ||
|
||||
changedProps.has("endTime") ||
|
||||
changedProps.has("unit") ||
|
||||
changedProps.has("logarithmicScale") ||
|
||||
changedProps.has("minYAxis") ||
|
||||
changedProps.has("maxYAxis") ||
|
||||
changedProps.has("fitYData") ||
|
||||
changedProps.has("_chartData") ||
|
||||
changedProps.has("paddingYAxis") ||
|
||||
changedProps.has("_yWidth")
|
||||
) {
|
||||
const rtl = computeRTL(this.hass);
|
||||
let minYAxis: number | ((values: { min: number }) => number) | undefined =
|
||||
this.minYAxis;
|
||||
let maxYAxis: number | ((values: { max: number }) => number) | undefined =
|
||||
this.maxYAxis;
|
||||
if (typeof minYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
|
||||
}
|
||||
if (typeof maxYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
|
||||
}
|
||||
this._chartOptions = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
min: this.startTime,
|
||||
max: this.endTime,
|
||||
},
|
||||
yAxis: {
|
||||
type: this.logarithmicScale ? "log" : "value",
|
||||
name: this.unit,
|
||||
min: this._clampYAxis(minYAxis),
|
||||
max: this._clampYAxis(maxYAxis),
|
||||
position: rtl ? "right" : "left",
|
||||
scale: true,
|
||||
nameGap: 2,
|
||||
nameTextStyle: {
|
||||
align: "left",
|
||||
},
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
margin: 5,
|
||||
formatter: (value: number) => {
|
||||
const label = formatNumber(value, this.hass.locale);
|
||||
const width = measureTextWidth(label, 12) + 5;
|
||||
if (width > this._yWidth) {
|
||||
this._yWidth = width;
|
||||
fireEvent(this, "y-width-changed", {
|
||||
value: this._yWidth,
|
||||
chartIndex: this.chartIndex,
|
||||
});
|
||||
}
|
||||
return label;
|
||||
},
|
||||
},
|
||||
} as YAXisOption,
|
||||
legend: {
|
||||
show: this.showNames,
|
||||
type: "scroll",
|
||||
animationDurationUpdate: 400,
|
||||
icon: "circle",
|
||||
padding: [20, 0],
|
||||
},
|
||||
grid: {
|
||||
...(this.showNames ? {} : { top: 30 }), // undefined is the same as 0
|
||||
left: rtl ? 1 : Math.max(this.paddingYAxis, this._yWidth),
|
||||
right: rtl ? Math.max(this.paddingYAxis, this._yWidth) : 1,
|
||||
bottom: 30,
|
||||
},
|
||||
visualMap: this._chartData
|
||||
.map((_, seriesIndex) => {
|
||||
const dataIndex = this._datasetToDataIndex[seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (!data.statistics || data.statistics.length === 0) {
|
||||
return false;
|
||||
}
|
||||
// render stat data with a slightly transparent line
|
||||
const firstStateTS =
|
||||
data.states[0]?.last_changed ?? this.endTime.getTime();
|
||||
return {
|
||||
show: false,
|
||||
seriesIndex,
|
||||
dimension: 0,
|
||||
pieces: [
|
||||
{
|
||||
max: firstStateTS - 0.01,
|
||||
colorAlpha: 0.5,
|
||||
},
|
||||
{
|
||||
min: firstStateTS,
|
||||
colorAlpha: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as VisualMapComponentOption[],
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
appendTo: document.body,
|
||||
formatter: this._renderTooltip.bind(this),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private _generateData() {
|
||||
let colorIndex = 0;
|
||||
const computedStyles = getComputedStyle(this);
|
||||
const entityStates = this.data;
|
||||
const datasets: ChartDataset<"line">[] = [];
|
||||
const datasets: LineSeriesOption[] = [];
|
||||
const entityIds: string[] = [];
|
||||
const datasetToDataIndex: number[] = [];
|
||||
if (entityStates.length === 0) {
|
||||
@@ -270,7 +336,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
// array containing [value1, value2, etc]
|
||||
let prevValues: any[] | null = null;
|
||||
|
||||
const data: ChartDataset<"line">[] = [];
|
||||
const data: LineSeriesOption[] = [];
|
||||
|
||||
const pushData = (timestamp: Date, datavalues: any[] | null) => {
|
||||
if (!datavalues) return;
|
||||
@@ -287,26 +353,45 @@ export class StateHistoryChartLine extends LitElement {
|
||||
// to the chart for the previous value. Otherwise the gap will
|
||||
// be too big. It will go from the start of the previous data
|
||||
// value until the start of the next data value.
|
||||
d.data.push({ x: timestamp.getTime(), y: prevValues[i] });
|
||||
d.data!.push([timestamp, prevValues[i]]);
|
||||
}
|
||||
d.data.push({ x: timestamp.getTime(), y: datavalues[i] });
|
||||
d.data!.push([timestamp, datavalues[i]]);
|
||||
});
|
||||
prevValues = datavalues;
|
||||
};
|
||||
|
||||
const addDataSet = (nameY: string, color?: string, fill = false) => {
|
||||
const addDataSet = (
|
||||
id: string,
|
||||
nameY: string,
|
||||
color?: string,
|
||||
fill = false
|
||||
) => {
|
||||
if (!color) {
|
||||
color = getGraphColorByIndex(colorIndex, computedStyles);
|
||||
colorIndex++;
|
||||
}
|
||||
data.push({
|
||||
label: nameY,
|
||||
fill: fill ? "origin" : false,
|
||||
borderColor: color,
|
||||
backgroundColor: color + "7F",
|
||||
stepped: "before",
|
||||
pointRadius: 0,
|
||||
id,
|
||||
data: [],
|
||||
type: "line",
|
||||
cursor: "default",
|
||||
name: nameY,
|
||||
color,
|
||||
symbol: "circle",
|
||||
step: "end",
|
||||
animationDurationUpdate: 0,
|
||||
symbolSize: 1,
|
||||
lineStyle: {
|
||||
width: fill ? 0 : 1.5,
|
||||
},
|
||||
areaStyle: fill
|
||||
? {
|
||||
color: color + "7F",
|
||||
}
|
||||
: undefined,
|
||||
tooltip: {
|
||||
show: !fill,
|
||||
},
|
||||
});
|
||||
entityIds.push(states.entity_id);
|
||||
datasetToDataIndex.push(dataIdx);
|
||||
@@ -324,12 +409,16 @@ export class StateHistoryChartLine extends LitElement {
|
||||
const isHeating =
|
||||
domain === "climate" && hasHvacAction
|
||||
? (entityState: LineChartState) =>
|
||||
entityState.attributes?.hvac_action === "heating"
|
||||
CLIMATE_HVAC_ACTION_TO_MODE[
|
||||
entityState.attributes?.hvac_action
|
||||
] === "heat"
|
||||
: (entityState: LineChartState) => entityState.state === "heat";
|
||||
const isCooling =
|
||||
domain === "climate" && hasHvacAction
|
||||
? (entityState: LineChartState) =>
|
||||
entityState.attributes?.hvac_action === "cooling"
|
||||
CLIMATE_HVAC_ACTION_TO_MODE[
|
||||
entityState.attributes?.hvac_action
|
||||
] === "cool"
|
||||
: (entityState: LineChartState) => entityState.state === "cool";
|
||||
|
||||
const hasHeat = states.states.some(isHeating);
|
||||
@@ -345,13 +434,23 @@ export class StateHistoryChartLine extends LitElement {
|
||||
entityState.attributes.target_temp_low
|
||||
);
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.climate.current_temperature", {
|
||||
name: name,
|
||||
})}`
|
||||
states.entity_id + "-current_temperature",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.current_temperature", {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.current_temperature.name"
|
||||
)
|
||||
);
|
||||
if (hasHeat) {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.climate.heating", { name: name })}`,
|
||||
states.entity_id + "-heating",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.heating", { name: name })
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.hvac_action.state.heating"
|
||||
),
|
||||
computedStyles.getPropertyValue("--state-climate-heat-color"),
|
||||
true
|
||||
);
|
||||
@@ -360,7 +459,12 @@ export class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
if (hasCool) {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.climate.cooling", { name: name })}`,
|
||||
states.entity_id + "-cooling",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.cooling", { name: name })
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.hvac_action.state.cooling"
|
||||
),
|
||||
computedStyles.getPropertyValue("--state-climate-cool-color"),
|
||||
true
|
||||
);
|
||||
@@ -370,22 +474,40 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
if (hasTargetRange) {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||
name: name,
|
||||
mode: this.hass.localize("ui.card.climate.high"),
|
||||
})}`
|
||||
states.entity_id + "-target_temperature_mode",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||
name: name,
|
||||
mode: this.hass.localize("ui.card.climate.high"),
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.target_temp_high.name"
|
||||
)
|
||||
);
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||
name: name,
|
||||
mode: this.hass.localize("ui.card.climate.low"),
|
||||
})}`
|
||||
states.entity_id + "-target_temperature_mode_low",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||
name: name,
|
||||
mode: this.hass.localize("ui.card.climate.low"),
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.target_temp_low.name"
|
||||
)
|
||||
);
|
||||
} else {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.climate.target_temperature_entity", {
|
||||
name: name,
|
||||
})}`
|
||||
states.entity_id + "-target_temperature",
|
||||
this.showNames
|
||||
? this.hass.localize(
|
||||
"ui.card.climate.target_temperature_entity",
|
||||
{
|
||||
name: name,
|
||||
}
|
||||
)
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.temperature.name"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -438,19 +560,29 @@ export class StateHistoryChartLine extends LitElement {
|
||||
);
|
||||
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.humidifier.target_humidity_entity", {
|
||||
name: name,
|
||||
})}`
|
||||
states.entity_id + "-target_humidity",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.humidifier.target_humidity_entity", {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.humidifier.entity_component._.state_attributes.humidity.name"
|
||||
)
|
||||
);
|
||||
|
||||
if (hasCurrent) {
|
||||
addDataSet(
|
||||
`${this.hass.localize(
|
||||
"ui.card.humidifier.current_humidity_entity",
|
||||
{
|
||||
name: name,
|
||||
}
|
||||
)}`
|
||||
states.entity_id + "-current_humidity",
|
||||
this.showNames
|
||||
? this.hass.localize(
|
||||
"ui.card.humidifier.current_humidity_entity",
|
||||
{
|
||||
name: name,
|
||||
}
|
||||
)
|
||||
: this.hass.localize(
|
||||
"component.humidifier.entity_component._.state_attributes.current_humidity.name"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -458,25 +590,40 @@ export class StateHistoryChartLine extends LitElement {
|
||||
// If action attribute is not available, we shade the area when the device is on
|
||||
if (hasHumidifying) {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.humidifier.humidifying", {
|
||||
name: name,
|
||||
})}`,
|
||||
states.entity_id + "-humidifying",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.humidifier.humidifying", {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.humidifier.entity_component._.state_attributes.action.state.humidifying"
|
||||
),
|
||||
computedStyles.getPropertyValue("--state-humidifier-on-color"),
|
||||
true
|
||||
);
|
||||
} else if (hasDrying) {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.humidifier.drying", {
|
||||
name: name,
|
||||
})}`,
|
||||
states.entity_id + "-drying",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.humidifier.drying", {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.humidifier.entity_component._.state_attributes.action.state.drying"
|
||||
),
|
||||
computedStyles.getPropertyValue("--state-humidifier-on-color"),
|
||||
true
|
||||
);
|
||||
} else {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.humidifier.on_entity", {
|
||||
name: name,
|
||||
})}`,
|
||||
states.entity_id + "-on",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.humidifier.on_entity", {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.humidifier.entity_component._.state.on"
|
||||
),
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
@@ -509,7 +656,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
pushData(new Date(entityState.last_changed), series);
|
||||
});
|
||||
} else {
|
||||
addDataSet(name);
|
||||
addDataSet(states.entity_id, name);
|
||||
|
||||
let lastValue: number;
|
||||
let lastDate: Date;
|
||||
@@ -575,12 +722,23 @@ export class StateHistoryChartLine extends LitElement {
|
||||
Array.prototype.push.apply(datasets, data);
|
||||
});
|
||||
|
||||
this._chartData = {
|
||||
datasets,
|
||||
};
|
||||
this._chartData = datasets;
|
||||
this._entityIds = entityIds;
|
||||
this._datasetToDataIndex = datasetToDataIndex;
|
||||
}
|
||||
|
||||
private _clampYAxis(value?: number | ((values: any) => number)) {
|
||||
if (this.logarithmicScale) {
|
||||
// log(0) is -Infinity, so we need to set a minimum value
|
||||
if (typeof value === "number") {
|
||||
return Math.max(value, 0.1);
|
||||
}
|
||||
if (typeof value === "function") {
|
||||
return (values: any) => Math.max(value(values), 0.1);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
customElements.define("state-history-chart-line", StateHistoryChartLine);
|
||||
|
||||
|
@@ -1,19 +1,26 @@
|
||||
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||
import { getRelativePosition } from "chart.js/helpers";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type {
|
||||
CustomSeriesOption,
|
||||
CustomSeriesRenderItem,
|
||||
ECElementEvent,
|
||||
TooltipFormatterCallback,
|
||||
TooltipPositionCallbackParams,
|
||||
} from "echarts/types/dist/shared";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { numberFormatToLocale } from "../../common/number/format_number";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { TimelineEntity } from "../../data/history";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import type { TimeLineData } from "./timeline-chart/const";
|
||||
import { computeTimelineColor } from "./timeline-chart/timeline-color";
|
||||
import { clickIsTouch } from "./click_is_touch";
|
||||
import { computeTimelineColor } from "./timeline-color";
|
||||
import type { ECOption } from "../../resources/echarts";
|
||||
import echarts from "../../resources/echarts";
|
||||
import { luminosity } from "../../common/color/rgb";
|
||||
import { hex2rgb } from "../../common/color/convert-color";
|
||||
import { measureTextWidth } from "../../util/text";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
@customElement("state-history-chart-timeline")
|
||||
export class StateHistoryChartTimeline extends LitElement {
|
||||
@@ -44,9 +51,9 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ attribute: false, type: Number }) public chartIndex?;
|
||||
|
||||
@state() private _chartData?: ChartData<"timeline">;
|
||||
@state() private _chartData: CustomSeriesOption[] = [];
|
||||
|
||||
@state() private _chartOptions?: ChartOptions<"timeline">;
|
||||
@state() private _chartOptions?: ECOption;
|
||||
|
||||
@state() private _yWidth = 0;
|
||||
|
||||
@@ -56,20 +63,99 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
return html`
|
||||
<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
.data=${this._chartData}
|
||||
.options=${this._chartOptions}
|
||||
.height=${this.data.length * 30 + 30}
|
||||
.paddingYAxis=${this.paddingYAxis - this._yWidth}
|
||||
chart-type="timeline"
|
||||
.height=${`${this.data.length * 30 + 30}px`}
|
||||
.data=${this._chartData as ECOption["series"]}
|
||||
@chart-click=${this._handleChartClick}
|
||||
></ha-chart-base>
|
||||
`;
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
this._createOptions();
|
||||
private _renderItem: CustomSeriesRenderItem = (params, api) => {
|
||||
const categoryIndex = api.value(0);
|
||||
const start = api.coord([api.value(1), categoryIndex]);
|
||||
const end = api.coord([api.value(2), categoryIndex]);
|
||||
const height = 20;
|
||||
const coordSys = params.coordSys as any;
|
||||
const rectShape = echarts.graphic.clipRectByRect(
|
||||
{
|
||||
x: start[0],
|
||||
y: start[1] - height / 2,
|
||||
width: end[0] - start[0],
|
||||
height: height,
|
||||
},
|
||||
{
|
||||
x: coordSys.x,
|
||||
y: coordSys.y,
|
||||
width: coordSys.width,
|
||||
height: coordSys.height,
|
||||
}
|
||||
);
|
||||
if (!rectShape) return null;
|
||||
const rect = {
|
||||
type: "rect" as const,
|
||||
transition: "shape" as const,
|
||||
shape: rectShape,
|
||||
style: {
|
||||
fill: api.value(4) as string,
|
||||
},
|
||||
};
|
||||
const text = api.value(3) as string;
|
||||
const textWidth = measureTextWidth(text, 12);
|
||||
const LABEL_PADDING = 4;
|
||||
if (textWidth < rectShape.width - LABEL_PADDING * 2) {
|
||||
return {
|
||||
type: "group",
|
||||
children: [
|
||||
rect,
|
||||
{
|
||||
type: "text",
|
||||
style: {
|
||||
...rectShape,
|
||||
x: rectShape.x + LABEL_PADDING,
|
||||
text,
|
||||
fill: api.value(5) as string,
|
||||
fontSize: 12,
|
||||
lineHeight: rectShape.height,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return rect;
|
||||
};
|
||||
|
||||
private _renderTooltip: TooltipFormatterCallback<TooltipPositionCallbackParams> =
|
||||
(params: TooltipPositionCallbackParams) => {
|
||||
const { value, name, marker, seriesName } = Array.isArray(params)
|
||||
? params[0]
|
||||
: params;
|
||||
const title = seriesName
|
||||
? `<h4 style="text-align: center; margin: 0;">${seriesName}</h4>`
|
||||
: "";
|
||||
const durationInMs = value![2] - value![1];
|
||||
const formattedDuration = `${this.hass.localize(
|
||||
"ui.components.history_charts.duration"
|
||||
)}: ${millisecondsToDuration(durationInMs)}`;
|
||||
|
||||
const lines = [
|
||||
marker + name,
|
||||
formatDateTimeWithSeconds(
|
||||
new Date(value![1]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
formatDateTimeWithSeconds(
|
||||
new Date(value![2]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
formattedDuration,
|
||||
].join("<br>");
|
||||
return [title, lines].join("");
|
||||
};
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
changedProps.has("startTime") ||
|
||||
changedProps.has("endTime") ||
|
||||
@@ -83,9 +169,12 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
}
|
||||
|
||||
if (
|
||||
!this.hasUpdated ||
|
||||
changedProps.has("startTime") ||
|
||||
changedProps.has("endTime") ||
|
||||
changedProps.has("showNames")
|
||||
changedProps.has("showNames") ||
|
||||
changedProps.has("paddingYAxis") ||
|
||||
changedProps.has("_yWidth")
|
||||
) {
|
||||
this._createOptions();
|
||||
}
|
||||
@@ -93,144 +182,71 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
private _createOptions() {
|
||||
const narrow = this.narrow;
|
||||
const showNames = this.chunked || this.showNames;
|
||||
const maxInternalLabelWidth = narrow ? 105 : 185;
|
||||
const labelWidth = showNames
|
||||
? Math.max(this.paddingYAxis, this._yWidth)
|
||||
: 0;
|
||||
const labelMargin = 5;
|
||||
const rtl = computeRTL(this.hass);
|
||||
this._chartOptions = {
|
||||
maintainAspectRatio: false,
|
||||
parsing: false,
|
||||
scales: {
|
||||
x: {
|
||||
type: "time",
|
||||
position: "bottom",
|
||||
adapters: {
|
||||
date: {
|
||||
locale: this.hass.locale,
|
||||
config: this.hass.config,
|
||||
},
|
||||
},
|
||||
min: this.startTime,
|
||||
suggestedMax: this.endTime,
|
||||
ticks: {
|
||||
autoSkip: true,
|
||||
maxRotation: 0,
|
||||
sampleSize: 5,
|
||||
autoSkipPadding: 20,
|
||||
major: {
|
||||
enabled: true,
|
||||
},
|
||||
font: (context) =>
|
||||
context.tick && context.tick.major
|
||||
? ({ weight: "bold" } as any)
|
||||
: {},
|
||||
},
|
||||
grid: {
|
||||
offset: false,
|
||||
},
|
||||
time: {
|
||||
tooltipFormat: "datetimeseconds",
|
||||
},
|
||||
xAxis: {
|
||||
type: "time",
|
||||
min: this.startTime,
|
||||
max: this.endTime,
|
||||
axisTick: {
|
||||
show: true,
|
||||
},
|
||||
y: {
|
||||
type: "category",
|
||||
barThickness: 20,
|
||||
offset: true,
|
||||
grid: {
|
||||
display: false,
|
||||
drawBorder: false,
|
||||
drawTicks: false,
|
||||
},
|
||||
ticks: {
|
||||
display: this.chunked || this.showNames,
|
||||
},
|
||||
afterSetDimensions: (y) => {
|
||||
y.maxWidth = y.chart.width * 0.18;
|
||||
},
|
||||
afterFit: (scaleInstance) => {
|
||||
if (this.chunked) {
|
||||
// ensure all the chart labels are the same width
|
||||
scaleInstance.width = narrow ? 105 : 185;
|
||||
}
|
||||
},
|
||||
afterUpdate: (y) => {
|
||||
const yWidth = this.showNames
|
||||
? (y.width ?? 0)
|
||||
: computeRTL(this.hass)
|
||||
? 0
|
||||
: (y.left ?? 0);
|
||||
if (
|
||||
this._yWidth !== Math.floor(yWidth) &&
|
||||
y.ticks.length === this.data.length
|
||||
) {
|
||||
this._yWidth = Math.floor(yWidth);
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: "category",
|
||||
inverse: true,
|
||||
position: rtl ? "right" : "left",
|
||||
triggerEvent: true,
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
show: showNames,
|
||||
width: labelWidth,
|
||||
overflow: "truncate",
|
||||
margin: labelMargin,
|
||||
formatter: (id: string) => {
|
||||
const label = this._chartData.find((d) => d.id === id)
|
||||
?.name as string;
|
||||
const width = label
|
||||
? Math.min(
|
||||
measureTextWidth(label, 12) + labelMargin,
|
||||
maxInternalLabelWidth
|
||||
)
|
||||
: 0;
|
||||
if (width > this._yWidth) {
|
||||
this._yWidth = width;
|
||||
fireEvent(this, "y-width-changed", {
|
||||
value: this._yWidth,
|
||||
chartIndex: this.chartIndex,
|
||||
});
|
||||
}
|
||||
return label;
|
||||
},
|
||||
position: computeRTL(this.hass) ? "right" : "left",
|
||||
hideOverlap: true,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
mode: "nearest",
|
||||
callbacks: {
|
||||
title: (context) =>
|
||||
context![0].chart!.data!.labels![
|
||||
context[0].datasetIndex
|
||||
] as string,
|
||||
beforeBody: (context) => context[0].dataset.label || "",
|
||||
label: (item) => {
|
||||
const d = item.dataset.data[item.dataIndex] as TimeLineData;
|
||||
const durationInMs = d.end.getTime() - d.start.getTime();
|
||||
const formattedDuration = `${this.hass.localize(
|
||||
"ui.components.history_charts.duration"
|
||||
)}: ${millisecondsToDuration(durationInMs)}`;
|
||||
|
||||
return [
|
||||
d.label || "",
|
||||
formatDateTimeWithSeconds(
|
||||
d.start,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
formatDateTimeWithSeconds(
|
||||
d.end,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
formattedDuration,
|
||||
];
|
||||
},
|
||||
labelColor: (item) => ({
|
||||
borderColor: (item.dataset.data[item.dataIndex] as TimeLineData)
|
||||
.color!,
|
||||
backgroundColor: (
|
||||
item.dataset.data[item.dataIndex] as TimeLineData
|
||||
).color!,
|
||||
}),
|
||||
},
|
||||
},
|
||||
filler: {
|
||||
propagate: true,
|
||||
},
|
||||
grid: {
|
||||
top: 10,
|
||||
bottom: 30,
|
||||
left: rtl ? 1 : labelWidth,
|
||||
right: rtl ? labelWidth : 1,
|
||||
},
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
onClick: (e: any) => {
|
||||
if (!this.clickForMoreInfo || clickIsTouch(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chart = e.chart;
|
||||
const canvasPosition = getRelativePosition(e, chart);
|
||||
|
||||
const index = Math.abs(
|
||||
chart.scales.y.getValueForPixel(canvasPosition.y)
|
||||
);
|
||||
fireEvent(this, "hass-more-info", {
|
||||
// @ts-ignore
|
||||
entityId: this._chartData?.datasets[index]?.label,
|
||||
});
|
||||
chart.canvas.dispatchEvent(new Event("mouseout")); // to hide tooltip
|
||||
tooltip: {
|
||||
appendTo: document.body,
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -246,8 +262,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
this._chartTime = new Date();
|
||||
const startTime = this.startTime;
|
||||
const endTime = this.endTime;
|
||||
const labels: string[] = [];
|
||||
const datasets: ChartDataset<"timeline">[] = [];
|
||||
const datasets: CustomSeriesOption[] = [];
|
||||
const names = this.names || {};
|
||||
// stateHistory is a list of lists of sorted state objects
|
||||
stateHistory.forEach((stateInfo) => {
|
||||
@@ -255,10 +270,11 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
let prevState: string | null = null;
|
||||
let locState: string | null = null;
|
||||
let prevLastChanged = startTime;
|
||||
const entityDisplay: string =
|
||||
names[stateInfo.entity_id] || stateInfo.name;
|
||||
const entityDisplay: string = this.showNames
|
||||
? names[stateInfo.entity_id] || stateInfo.name || stateInfo.entity_id
|
||||
: "";
|
||||
|
||||
const dataRow: TimeLineData[] = [];
|
||||
const dataRow: unknown[] = [];
|
||||
stateInfo.data.forEach((entityState) => {
|
||||
let newState: string | null = entityState.state;
|
||||
const timeStamp = new Date(entityState.last_changed);
|
||||
@@ -277,15 +293,23 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
} else if (newState !== prevState) {
|
||||
newLastChanged = new Date(entityState.last_changed);
|
||||
|
||||
const color = computeTimelineColor(
|
||||
prevState,
|
||||
computedStyles,
|
||||
this.hass.states[stateInfo.entity_id]
|
||||
);
|
||||
dataRow.push({
|
||||
start: prevLastChanged,
|
||||
end: newLastChanged,
|
||||
label: locState,
|
||||
color: computeTimelineColor(
|
||||
prevState,
|
||||
computedStyles,
|
||||
this.hass.states[stateInfo.entity_id]
|
||||
),
|
||||
value: [
|
||||
stateInfo.entity_id,
|
||||
prevLastChanged,
|
||||
newLastChanged,
|
||||
locState,
|
||||
color,
|
||||
luminosity(hex2rgb(color)) > 0.5 ? "#000" : "#fff",
|
||||
],
|
||||
itemStyle: {
|
||||
color,
|
||||
},
|
||||
});
|
||||
|
||||
prevState = newState;
|
||||
@@ -295,28 +319,52 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
});
|
||||
|
||||
if (prevState !== null) {
|
||||
const color = computeTimelineColor(
|
||||
prevState,
|
||||
computedStyles,
|
||||
this.hass.states[stateInfo.entity_id]
|
||||
);
|
||||
dataRow.push({
|
||||
start: prevLastChanged,
|
||||
end: endTime,
|
||||
label: locState,
|
||||
color: computeTimelineColor(
|
||||
prevState,
|
||||
computedStyles,
|
||||
this.hass.states[stateInfo.entity_id]
|
||||
),
|
||||
value: [
|
||||
stateInfo.entity_id,
|
||||
prevLastChanged,
|
||||
endTime,
|
||||
locState,
|
||||
color,
|
||||
luminosity(hex2rgb(color)) > 0.5 ? "#000" : "#fff",
|
||||
],
|
||||
itemStyle: {
|
||||
color,
|
||||
},
|
||||
});
|
||||
}
|
||||
datasets.push({
|
||||
id: stateInfo.entity_id,
|
||||
data: dataRow,
|
||||
label: stateInfo.entity_id,
|
||||
name: entityDisplay,
|
||||
dimensions: ["id", "start", "end", "name", "color", "textColor"],
|
||||
type: "custom",
|
||||
encode: {
|
||||
x: [1, 2],
|
||||
y: 0,
|
||||
itemName: 3,
|
||||
},
|
||||
renderItem: this._renderItem,
|
||||
});
|
||||
labels.push(entityDisplay);
|
||||
});
|
||||
|
||||
this._chartData = {
|
||||
labels: labels,
|
||||
datasets: datasets,
|
||||
};
|
||||
this._chartData = datasets;
|
||||
}
|
||||
|
||||
private _handleChartClick(e: CustomEvent<ECElementEvent>): void {
|
||||
if (e.detail.targetType === "axisLabel") {
|
||||
const dataset = this._chartData[e.detail.dataIndex];
|
||||
if (dataset) {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: dataset.id as string,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
@@ -69,6 +69,8 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
|
||||
|
||||
@property({ type: String }) public height?: string;
|
||||
|
||||
private _computedStartTime!: Date;
|
||||
|
||||
private _computedEndTime!: Date;
|
||||
@@ -133,7 +135,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
if (!Array.isArray(item)) {
|
||||
return html`<div class="entry-container">
|
||||
return html`<div class="entry-container line">
|
||||
<state-history-chart-line
|
||||
.hass=${this.hass}
|
||||
.unit=${item.unit}
|
||||
@@ -151,10 +153,11 @@ export class StateHistoryCharts extends LitElement {
|
||||
.maxYAxis=${this.maxYAxis}
|
||||
.fitYData=${this.fitYData}
|
||||
@y-width-changed=${this._yWidthChanged}
|
||||
.height=${this.virtualize ? undefined : this.height}
|
||||
></state-history-chart-line>
|
||||
</div> `;
|
||||
}
|
||||
return html`<div class="entry-container">
|
||||
return html`<div class="entry-container timeline">
|
||||
<state-history-chart-timeline
|
||||
.hass=${this.hass}
|
||||
.data=${item}
|
||||
@@ -274,7 +277,8 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* height of single timeline chart = 60px */
|
||||
min-height: 60px;
|
||||
}
|
||||
@@ -297,6 +301,10 @@ export class StateHistoryCharts extends LitElement {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entry-container.line {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.entry-container:hover {
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -308,6 +316,10 @@ export class StateHistoryCharts extends LitElement {
|
||||
padding-inline-end: 1px;
|
||||
}
|
||||
|
||||
.entry-container.timeline:first-child {
|
||||
margin-top: var(--timeline-top-margin);
|
||||
}
|
||||
|
||||
.entry-container:not(:first-child) {
|
||||
border-top: 2px solid var(--divider-color);
|
||||
margin-top: 16px;
|
||||
|
@@ -1,21 +1,22 @@
|
||||
import type {
|
||||
ChartData,
|
||||
ChartDataset,
|
||||
ChartOptions,
|
||||
ChartType,
|
||||
} from "chart.js";
|
||||
BarSeriesOption,
|
||||
LineSeriesOption,
|
||||
} from "echarts/types/dist/shared";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import {
|
||||
formatNumber,
|
||||
numberFormatToLocale,
|
||||
getNumberFormatOptions,
|
||||
} from "../../common/number/format_number";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type {
|
||||
Statistics,
|
||||
StatisticsMetaData,
|
||||
@@ -25,13 +26,11 @@ import {
|
||||
getDisplayUnit,
|
||||
getStatisticLabel,
|
||||
getStatisticMetadata,
|
||||
isExternalStatistic,
|
||||
statisticsHaveType,
|
||||
} from "../../data/recorder";
|
||||
import type { ECOption } from "../../resources/echarts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-chart-base";
|
||||
import type { ChartDatasetExtra } from "./ha-chart-base";
|
||||
import { clickIsTouch } from "./click_is_touch";
|
||||
|
||||
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
||||
mean: "mean",
|
||||
@@ -57,12 +56,14 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
@property() public unit?: string;
|
||||
|
||||
@property({ attribute: false }) public startTime?: Date;
|
||||
|
||||
@property({ attribute: false }) public endTime?: Date;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
public statTypes: StatisticType[] = ["sum", "min", "mean", "max"];
|
||||
|
||||
@property({ attribute: false }) public chartType: ChartType = "line";
|
||||
@property({ attribute: false }) public chartType: "line" | "bar" = "line";
|
||||
|
||||
@property({ attribute: false, type: Number }) public minYAxis?: number;
|
||||
|
||||
@@ -84,13 +85,18 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
@property() public period?: string;
|
||||
|
||||
@state() private _chartData: ChartData = { datasets: [] };
|
||||
@property({ attribute: "days-to-show", type: Number })
|
||||
public daysToShow?: number;
|
||||
|
||||
@state() private _chartDatasetExtra: ChartDatasetExtra[] = [];
|
||||
@property({ type: String }) public height?: string;
|
||||
|
||||
@state() private _chartData: (LineSeriesOption | BarSeriesOption)[] = [];
|
||||
|
||||
@state() private _legendData: string[] = [];
|
||||
|
||||
@state() private _statisticIds: string[] = [];
|
||||
|
||||
@state() private _chartOptions?: ChartOptions;
|
||||
@state() private _chartOptions?: ECOption;
|
||||
|
||||
@state() private _hiddenStats = new Set<string>();
|
||||
|
||||
@@ -101,8 +107,14 @@ export class StatisticsChart extends LitElement {
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (changedProps.has("legendMode")) {
|
||||
this._hiddenStats.clear();
|
||||
if (
|
||||
changedProps.has("statisticsData") ||
|
||||
changedProps.has("statTypes") ||
|
||||
changedProps.has("chartType") ||
|
||||
changedProps.has("hideLegend") ||
|
||||
changedProps.has("_hiddenStats")
|
||||
) {
|
||||
this._generateData();
|
||||
}
|
||||
if (
|
||||
!this.hasUpdated ||
|
||||
@@ -113,19 +125,14 @@ export class StatisticsChart extends LitElement {
|
||||
changedProps.has("maxYAxis") ||
|
||||
changedProps.has("fitYData") ||
|
||||
changedProps.has("logarithmicScale") ||
|
||||
changedProps.has("hideLegend")
|
||||
changedProps.has("hideLegend") ||
|
||||
changedProps.has("startTime") ||
|
||||
changedProps.has("endTime") ||
|
||||
changedProps.has("_legendData") ||
|
||||
changedProps.has("_chartData")
|
||||
) {
|
||||
this._createOptions();
|
||||
}
|
||||
if (
|
||||
changedProps.has("statisticsData") ||
|
||||
changedProps.has("statTypes") ||
|
||||
changedProps.has("chartType") ||
|
||||
changedProps.has("hideLegend") ||
|
||||
changedProps.has("_hiddenStats")
|
||||
) {
|
||||
this._generateData();
|
||||
}
|
||||
}
|
||||
|
||||
public firstUpdated() {
|
||||
@@ -157,145 +164,158 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-chart-base
|
||||
external-hidden
|
||||
.hass=${this.hass}
|
||||
.data=${this._chartData}
|
||||
.extraData=${this._chartDatasetExtra}
|
||||
.options=${this._chartOptions}
|
||||
.chartType=${this.chartType}
|
||||
.height=${this.height}
|
||||
style=${styleMap({ height: this.height })}
|
||||
external-hidden
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
></ha-chart-base>
|
||||
`;
|
||||
}
|
||||
|
||||
private _datasetHidden(ev) {
|
||||
ev.stopPropagation();
|
||||
this._hiddenStats.add(this._statisticIds[ev.detail.index]);
|
||||
private _datasetHidden(ev: CustomEvent) {
|
||||
this._hiddenStats.add(ev.detail.name);
|
||||
this.requestUpdate("_hiddenStats");
|
||||
}
|
||||
|
||||
private _datasetUnhidden(ev) {
|
||||
ev.stopPropagation();
|
||||
this._hiddenStats.delete(this._statisticIds[ev.detail.index]);
|
||||
private _datasetUnhidden(ev: CustomEvent) {
|
||||
this._hiddenStats.delete(ev.detail.name);
|
||||
this.requestUpdate("_hiddenStats");
|
||||
}
|
||||
|
||||
private _createOptions(unit?: string) {
|
||||
this._chartOptions = {
|
||||
parsing: false,
|
||||
interaction: {
|
||||
mode: "nearest",
|
||||
axis: "x",
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "time",
|
||||
adapters: {
|
||||
date: {
|
||||
locale: this.hass.locale,
|
||||
config: this.hass.config,
|
||||
},
|
||||
},
|
||||
ticks: {
|
||||
source: this.chartType === "bar" ? "data" : undefined,
|
||||
maxRotation: 0,
|
||||
sampleSize: 5,
|
||||
autoSkipPadding: 20,
|
||||
major: {
|
||||
enabled: true,
|
||||
},
|
||||
font: (context) =>
|
||||
context.tick && context.tick.major
|
||||
? ({ weight: "bold" } as any)
|
||||
: {},
|
||||
},
|
||||
time: {
|
||||
tooltipFormat: "datetime",
|
||||
unit:
|
||||
this.chartType === "bar" &&
|
||||
this.period &&
|
||||
["hour", "day", "week", "month"].includes(this.period)
|
||||
? this.period
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: this.chartType === "bar",
|
||||
ticks: {
|
||||
maxTicksLimit: 7,
|
||||
},
|
||||
title: {
|
||||
display: unit || this.unit,
|
||||
text: unit || this.unit,
|
||||
},
|
||||
type: this.logarithmicScale ? "logarithmic" : "linear",
|
||||
min: this.fitYData ? null : this.minYAxis,
|
||||
max: this.fitYData ? null : this.maxYAxis,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) =>
|
||||
`${context.dataset.label}: ${formatNumber(
|
||||
context.parsed.y,
|
||||
private _renderTooltip = (params: any) => {
|
||||
const rendered: Record<string, boolean> = {};
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
return params
|
||||
.map((param, index: number) => {
|
||||
if (rendered[param.seriesName]) return "";
|
||||
rendered[param.seriesName] = true;
|
||||
|
||||
const statisticId = this._statisticIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
const entry = this.hass.entities[statisticId];
|
||||
// max series can have 3 values, as the second value is the max-min to form a band
|
||||
const rawValue = String(param.value[2] ?? param.value[1]);
|
||||
|
||||
const options = getNumberFormatOptions(stateObj, entry) ?? {
|
||||
maximumFractionDigits: 2,
|
||||
};
|
||||
|
||||
const value = `${formatNumber(
|
||||
rawValue,
|
||||
this.hass.locale,
|
||||
options
|
||||
)}${unit}`;
|
||||
|
||||
const time =
|
||||
index === 0
|
||||
? formatDateTimeWithSeconds(
|
||||
new Date(param.value[0]),
|
||||
this.hass.locale,
|
||||
getNumberFormatOptions(
|
||||
undefined,
|
||||
this.hass.entities[this._statisticIds[context.datasetIndex]]
|
||||
)
|
||||
)} ${
|
||||
// @ts-ignore
|
||||
context.dataset.unit || ""
|
||||
}`,
|
||||
},
|
||||
},
|
||||
filler: {
|
||||
propagate: true,
|
||||
},
|
||||
legend: {
|
||||
display: !this.hideLegend,
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0.4,
|
||||
cubicInterpolationMode: "monotone",
|
||||
borderWidth: 1.5,
|
||||
},
|
||||
bar: { borderWidth: 1.5, borderRadius: 4 },
|
||||
point: {
|
||||
hitRadius: 50,
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
onClick: (e: any) => {
|
||||
if (!this.clickForMoreInfo || clickIsTouch(e)) {
|
||||
return;
|
||||
this.hass.config
|
||||
) + "<br>"
|
||||
: "";
|
||||
return `${time}${param.marker} ${param.seriesName}: ${value}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("<br>");
|
||||
};
|
||||
|
||||
private _createOptions() {
|
||||
const dayDifference = this.daysToShow ?? 1;
|
||||
let minYAxis: number | ((values: { min: number }) => number) | undefined =
|
||||
this.minYAxis;
|
||||
let maxYAxis: number | ((values: { max: number }) => number) | undefined =
|
||||
this.maxYAxis;
|
||||
if (typeof minYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
|
||||
}
|
||||
if (typeof maxYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
|
||||
}
|
||||
const endTime = this.endTime ?? new Date();
|
||||
let startTime = this.startTime;
|
||||
|
||||
if (!startTime) {
|
||||
// set start time to the earliest point in the chart data
|
||||
this._chartData.forEach((series) => {
|
||||
if (!Array.isArray(series.data) || !series.data[0]) return;
|
||||
const firstPoint = series.data[0] as any;
|
||||
const timestamp = Array.isArray(firstPoint)
|
||||
? firstPoint[0]
|
||||
: firstPoint.value?.[0];
|
||||
if (timestamp && (!startTime || new Date(timestamp) < startTime)) {
|
||||
startTime = new Date(timestamp);
|
||||
}
|
||||
});
|
||||
|
||||
const chart = e.chart;
|
||||
|
||||
const points = chart.getElementsAtEventForMode(
|
||||
e,
|
||||
"nearest",
|
||||
{ intersect: true },
|
||||
true
|
||||
if (!startTime) {
|
||||
// Calculate default start time based on dayDifference
|
||||
startTime = new Date(
|
||||
endTime.getTime() - dayDifference * 24 * 3600 * 1000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (points.length) {
|
||||
const firstPoint = points[0];
|
||||
const statisticId = this._statisticIds[firstPoint.datasetIndex];
|
||||
if (!isExternalStatistic(statisticId)) {
|
||||
fireEvent(this, "hass-more-info", { entityId: statisticId });
|
||||
chart.canvas.dispatchEvent(new Event("mouseout")); // to hide tooltip
|
||||
}
|
||||
}
|
||||
this._chartOptions = {
|
||||
xAxis: [
|
||||
{
|
||||
type: "time",
|
||||
min: startTime,
|
||||
max: endTime,
|
||||
},
|
||||
{
|
||||
type: "time",
|
||||
show: false,
|
||||
},
|
||||
],
|
||||
yAxis: {
|
||||
type: this.logarithmicScale ? "log" : "value",
|
||||
name: this.unit,
|
||||
nameGap: 2,
|
||||
nameTextStyle: {
|
||||
align: "left",
|
||||
},
|
||||
position: computeRTL(this.hass) ? "right" : "left",
|
||||
scale: true,
|
||||
min: this._clampYAxis(minYAxis),
|
||||
max: this._clampYAxis(maxYAxis),
|
||||
splitLine: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: !this.hideLegend,
|
||||
type: "scroll",
|
||||
animationDurationUpdate: 400,
|
||||
icon: "circle",
|
||||
padding: [20, 0],
|
||||
data: this._legendData,
|
||||
},
|
||||
grid: {
|
||||
...(this.hideLegend ? { top: this.unit ? 30 : 5 } : {}), // undefined is the same as 0
|
||||
left: 1,
|
||||
right: 1,
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
appendTo: document.body,
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -325,8 +345,8 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
let colorIndex = 0;
|
||||
const statisticsData = Object.entries(this.statisticsData);
|
||||
const totalDataSets: ChartDataset<"line">[] = [];
|
||||
const totalDatasetExtras: ChartDatasetExtra[] = [];
|
||||
const totalDataSets: typeof this._chartData = [];
|
||||
const legendData: { name: string; color: string }[] = [];
|
||||
const statisticIds: string[] = [];
|
||||
let endTime: Date;
|
||||
|
||||
@@ -348,6 +368,7 @@ export class StatisticsChart extends LitElement {
|
||||
if (endTime > new Date()) {
|
||||
endTime = new Date();
|
||||
}
|
||||
this.endTime = endTime;
|
||||
|
||||
let unit: string | undefined | null;
|
||||
|
||||
@@ -372,19 +393,19 @@ export class StatisticsChart extends LitElement {
|
||||
}
|
||||
|
||||
// array containing [value1, value2, etc]
|
||||
let prevValues: (number | null)[] | null = null;
|
||||
let prevValues: (number | null)[][] | null = null;
|
||||
let prevEndTime: Date | undefined;
|
||||
|
||||
// The datasets for the current statistic
|
||||
const statDataSets: ChartDataset<"line">[] = [];
|
||||
const statDatasetExtras: ChartDatasetExtra[] = [];
|
||||
const statDataSets: (LineSeriesOption | BarSeriesOption)[] = [];
|
||||
const statLegendData: { name: string; color: string }[] = [];
|
||||
|
||||
const pushData = (
|
||||
start: Date,
|
||||
end: Date,
|
||||
dataValues: (number | null)[] | null
|
||||
dataValues: (number | null)[][]
|
||||
) => {
|
||||
if (!dataValues) return;
|
||||
if (!dataValues.length) return;
|
||||
if (start > end) {
|
||||
// Drop data points that are after the requested endTime. This could happen if
|
||||
// endTime is "now" and client time is not in sync with server time.
|
||||
@@ -399,11 +420,12 @@ export class StatisticsChart extends LitElement {
|
||||
) {
|
||||
// if the end of the previous data doesn't match the start of the current data,
|
||||
// we have to draw a gap so add a value at the end time, and then an empty value.
|
||||
d.data.push({ x: prevEndTime.getTime(), y: prevValues[i]! });
|
||||
// @ts-expect-error
|
||||
d.data.push({ x: prevEndTime.getTime(), y: null });
|
||||
d.data!.push(
|
||||
this._transformDataValue([prevEndTime, ...prevValues[i]!])
|
||||
);
|
||||
d.data!.push([prevEndTime, null]);
|
||||
}
|
||||
d.data.push({ x: start.getTime(), y: dataValues[i]! });
|
||||
d.data!.push(this._transformDataValue([start, ...dataValues[i]!]));
|
||||
});
|
||||
prevValues = dataValues;
|
||||
prevEndTime = end;
|
||||
@@ -438,49 +460,64 @@ export class StatisticsChart extends LitElement {
|
||||
})
|
||||
: this.statTypes;
|
||||
|
||||
let displayed_legend = false;
|
||||
let displayedLegend = false;
|
||||
sortedTypes.forEach((type) => {
|
||||
if (statisticsHaveType(stats, type)) {
|
||||
const band = drawBands && (type === "min" || type === "max");
|
||||
if (!this.hideLegend) {
|
||||
const show_legend = hasMean
|
||||
const showLegend = hasMean
|
||||
? type === "mean"
|
||||
: displayed_legend === false;
|
||||
statDatasetExtras.push({
|
||||
legend_label: name,
|
||||
show_legend,
|
||||
});
|
||||
displayed_legend = displayed_legend || show_legend;
|
||||
: displayedLegend === false;
|
||||
if (showLegend) {
|
||||
statLegendData.push({ name, color });
|
||||
}
|
||||
displayedLegend = displayedLegend || showLegend;
|
||||
}
|
||||
statTypes.push(type);
|
||||
statDataSets.push({
|
||||
label: name
|
||||
const borderColor =
|
||||
band && hasMean ? color + (this.hideLegend ? "00" : "7F") : color;
|
||||
const backgroundColor = band ? color + "3F" : color + "7F";
|
||||
const series: LineSeriesOption | BarSeriesOption = {
|
||||
id: `${statistic_id}-${type}`,
|
||||
type: this.chartType,
|
||||
smooth: this.chartType === "line" ? 0.4 : false,
|
||||
smoothMonotone: "x",
|
||||
cursor: "default",
|
||||
data: [],
|
||||
name: name
|
||||
? `${name} (${this.hass.localize(
|
||||
`ui.components.statistics_charts.statistic_types.${type}`
|
||||
)})
|
||||
`
|
||||
)})`
|
||||
: this.hass.localize(
|
||||
`ui.components.statistics_charts.statistic_types.${type}`
|
||||
),
|
||||
fill: drawBands
|
||||
? type === "min" && hasMean
|
||||
? "+1"
|
||||
: type === "max"
|
||||
? "-1"
|
||||
: false
|
||||
: false,
|
||||
borderColor:
|
||||
band && hasMean ? color + (this.hideLegend ? "00" : "7F") : color,
|
||||
backgroundColor: band ? color + "3F" : color + "7F",
|
||||
pointRadius: 0,
|
||||
hidden: !this.hideLegend
|
||||
? this._hiddenStats.has(statistic_id)
|
||||
: false,
|
||||
data: [],
|
||||
// @ts-ignore
|
||||
unit: meta?.unit_of_measurement,
|
||||
band,
|
||||
});
|
||||
symbol: "circle",
|
||||
symbolSize: 0,
|
||||
animationDurationUpdate: 0,
|
||||
lineStyle: {
|
||||
width: 1.5,
|
||||
},
|
||||
itemStyle:
|
||||
this.chartType === "bar"
|
||||
? {
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderColor,
|
||||
borderWidth: 1.5,
|
||||
}
|
||||
: undefined,
|
||||
color: this.chartType === "bar" ? backgroundColor : borderColor,
|
||||
};
|
||||
if (band && this.chartType === "line") {
|
||||
series.stack = `band-${statistic_id}`;
|
||||
series.stackStrategy = "all";
|
||||
(series as LineSeriesOption).symbol = "none";
|
||||
if (drawBands && type === "max") {
|
||||
(series as LineSeriesOption).areaStyle = {
|
||||
color: color + "3F",
|
||||
};
|
||||
}
|
||||
}
|
||||
statDataSets.push(series);
|
||||
statisticIds.push(statistic_id);
|
||||
}
|
||||
});
|
||||
@@ -494,40 +531,79 @@ export class StatisticsChart extends LitElement {
|
||||
return;
|
||||
}
|
||||
prevDate = startDate;
|
||||
const dataValues: (number | null)[] = [];
|
||||
const dataValues: (number | null)[][] = [];
|
||||
statTypes.forEach((type) => {
|
||||
let val: number | null | undefined;
|
||||
const val: (number | null)[] = [];
|
||||
if (type === "sum") {
|
||||
if (firstSum === null || firstSum === undefined) {
|
||||
val = 0;
|
||||
val.push(0);
|
||||
firstSum = stat.sum;
|
||||
} else {
|
||||
val = (stat.sum || 0) - firstSum;
|
||||
val.push((stat.sum || 0) - firstSum);
|
||||
}
|
||||
} else if (type === "max" && this.chartType === "line") {
|
||||
const max = stat.max || 0;
|
||||
val.push(Math.abs(max - (stat.min || 0)));
|
||||
val.push(max);
|
||||
} else {
|
||||
val = stat[type];
|
||||
val.push(stat[type] ?? null);
|
||||
}
|
||||
dataValues.push(val ?? null);
|
||||
dataValues.push(val);
|
||||
});
|
||||
pushData(startDate, new Date(stat.end), dataValues);
|
||||
if (!this._hiddenStats.has(name)) {
|
||||
pushData(startDate, new Date(stat.end), dataValues);
|
||||
}
|
||||
});
|
||||
|
||||
// Concat two arrays
|
||||
Array.prototype.push.apply(totalDataSets, statDataSets);
|
||||
Array.prototype.push.apply(totalDatasetExtras, statDatasetExtras);
|
||||
Array.prototype.push.apply(legendData, statLegendData);
|
||||
});
|
||||
|
||||
if (unit) {
|
||||
this._createOptions(unit);
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
this._chartData = {
|
||||
datasets: totalDataSets,
|
||||
};
|
||||
this._chartDatasetExtra = totalDatasetExtras;
|
||||
legendData.forEach(({ name, color }) => {
|
||||
// Add an empty series for the legend
|
||||
totalDataSets.push({
|
||||
id: name + "-legend",
|
||||
name: name,
|
||||
color,
|
||||
type: this.chartType,
|
||||
data: [],
|
||||
xAxisIndex: 1,
|
||||
});
|
||||
});
|
||||
|
||||
this._chartData = totalDataSets;
|
||||
if (legendData.length !== this._legendData.length) {
|
||||
// only update the legend if it has changed or it will trigger options update
|
||||
this._legendData = legendData.map(({ name }) => name);
|
||||
}
|
||||
this._statisticIds = statisticIds;
|
||||
}
|
||||
|
||||
private _transformDataValue(val: [Date, ...(number | null)[]]) {
|
||||
if (this.chartType === "bar" && val[1] && val[1] < 0) {
|
||||
return { value: val, itemStyle: { borderRadius: [0, 0, 4, 4] } };
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
private _clampYAxis(value?: number | ((values: any) => number)) {
|
||||
if (this.logarithmicScale) {
|
||||
// log(0) is -Infinity, so we need to set a minimum value
|
||||
if (typeof value === "number") {
|
||||
return Math.max(value, 0.1);
|
||||
}
|
||||
if (typeof value === "function") {
|
||||
return (values: any) => Math.max(value(values), 0.1);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
|
@@ -1,22 +0,0 @@
|
||||
import type {
|
||||
BarControllerChartOptions,
|
||||
BarControllerDatasetOptions,
|
||||
} from "chart.js";
|
||||
|
||||
export interface TimeLineData {
|
||||
start: Date;
|
||||
end: Date;
|
||||
label?: string | null;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
declare module "chart.js" {
|
||||
interface ChartTypeRegistry {
|
||||
timeline: {
|
||||
chartOptions: BarControllerChartOptions;
|
||||
datasetOptions: BarControllerDatasetOptions;
|
||||
defaultDataPoint: TimeLineData;
|
||||
parsedDataType: any;
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,63 +0,0 @@
|
||||
import type { BarOptions, BarProps } from "chart.js";
|
||||
import { BarElement } from "chart.js";
|
||||
import { hex2rgb } from "../../../common/color/convert-color";
|
||||
import { luminosity } from "../../../common/color/rgb";
|
||||
|
||||
export interface TextBarProps extends BarProps {
|
||||
text?: string | null;
|
||||
options?: Partial<TextBaroptions>;
|
||||
}
|
||||
|
||||
export interface TextBaroptions extends BarOptions {
|
||||
textPad?: number;
|
||||
textColor?: string;
|
||||
backgroundColor: string;
|
||||
}
|
||||
|
||||
export class TextBarElement extends BarElement {
|
||||
static id = "textbar";
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
super.draw(ctx);
|
||||
const options = this.options as TextBaroptions;
|
||||
const { x, y, base, width, text } = (
|
||||
this as BarElement<TextBarProps, TextBaroptions>
|
||||
).getProps(["x", "y", "base", "width", "text"]);
|
||||
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
const textRect = ctx.measureText(text);
|
||||
if (
|
||||
textRect.width === 0 ||
|
||||
textRect.width + (options.textPad || 4) + 2 > width
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const textColor =
|
||||
options.textColor ||
|
||||
(options?.backgroundColor === "transparent"
|
||||
? "transparent"
|
||||
: luminosity(hex2rgb(options.backgroundColor)) > 0.5
|
||||
? "#000"
|
||||
: "#fff");
|
||||
|
||||
// ctx.font = "12px arial";
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.lineWidth = 0;
|
||||
ctx.strokeStyle = textColor;
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(
|
||||
text,
|
||||
x - width / 2 + (options.textPad || 4),
|
||||
y + (base - y) / 2
|
||||
);
|
||||
}
|
||||
|
||||
tooltipPosition(useFinalPosition: boolean) {
|
||||
const { x, y, base } = this.getProps(["x", "y", "base"], useFinalPosition);
|
||||
return { x, y: y + (base - y) / 2 };
|
||||
}
|
||||
}
|
@@ -1,255 +0,0 @@
|
||||
import type { BarElement } from "chart.js";
|
||||
import { BarController } from "chart.js";
|
||||
import type { TimeLineData } from "./const";
|
||||
import type { TextBarProps } from "./textbar-element";
|
||||
|
||||
function borderProps(properties) {
|
||||
let reverse;
|
||||
let start;
|
||||
let end;
|
||||
let top;
|
||||
let bottom;
|
||||
if (properties.horizontal) {
|
||||
reverse = properties.base > properties.x;
|
||||
start = "left";
|
||||
end = "right";
|
||||
} else {
|
||||
reverse = properties.base < properties.y;
|
||||
start = "bottom";
|
||||
end = "top";
|
||||
}
|
||||
if (reverse) {
|
||||
top = "end";
|
||||
bottom = "start";
|
||||
} else {
|
||||
top = "start";
|
||||
bottom = "end";
|
||||
}
|
||||
return { start, end, reverse, top, bottom };
|
||||
}
|
||||
|
||||
function setBorderSkipped(properties, options, stack, index) {
|
||||
let edge = options.borderSkipped;
|
||||
const res = {};
|
||||
|
||||
if (!edge) {
|
||||
properties.borderSkipped = res;
|
||||
return;
|
||||
}
|
||||
|
||||
if (edge === true) {
|
||||
properties.borderSkipped = {
|
||||
top: true,
|
||||
right: true,
|
||||
bottom: true,
|
||||
left: true,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const { start, end, reverse, top, bottom } = borderProps(properties);
|
||||
|
||||
if (edge === "middle" && stack) {
|
||||
properties.enableBorderRadius = true;
|
||||
if ((stack._top || 0) === index) {
|
||||
edge = top;
|
||||
} else if ((stack._bottom || 0) === index) {
|
||||
edge = bottom;
|
||||
} else {
|
||||
res[parseEdge(bottom, start, end, reverse)] = true;
|
||||
edge = top;
|
||||
}
|
||||
}
|
||||
|
||||
res[parseEdge(edge, start, end, reverse)] = true;
|
||||
properties.borderSkipped = res;
|
||||
}
|
||||
|
||||
function parseEdge(edge, a, b, reverse) {
|
||||
if (reverse) {
|
||||
edge = swap(edge, a, b);
|
||||
edge = startEnd(edge, b, a);
|
||||
} else {
|
||||
edge = startEnd(edge, a, b);
|
||||
}
|
||||
return edge;
|
||||
}
|
||||
|
||||
function swap(orig, v1, v2) {
|
||||
return orig === v1 ? v2 : orig === v2 ? v1 : orig;
|
||||
}
|
||||
|
||||
function startEnd(v, start, end) {
|
||||
return v === "start" ? start : v === "end" ? end : v;
|
||||
}
|
||||
|
||||
function setInflateAmount(
|
||||
properties,
|
||||
{ inflateAmount }: { inflateAmount?: string | number },
|
||||
ratio
|
||||
) {
|
||||
properties.inflateAmount =
|
||||
inflateAmount === "auto" ? (ratio === 1 ? 0.33 : 0) : inflateAmount;
|
||||
}
|
||||
|
||||
function parseValue(entry, item, vScale, i) {
|
||||
const startValue = vScale.parse(entry.start, i);
|
||||
const endValue = vScale.parse(entry.end, i);
|
||||
const min = Math.min(startValue, endValue);
|
||||
const max = Math.max(startValue, endValue);
|
||||
let barStart = min;
|
||||
let barEnd = max;
|
||||
|
||||
if (Math.abs(min) > Math.abs(max)) {
|
||||
barStart = max;
|
||||
barEnd = min;
|
||||
}
|
||||
|
||||
// Store `barEnd` (furthest away from origin) as parsed value,
|
||||
// to make stacking straight forward
|
||||
item[vScale.axis] = barEnd;
|
||||
|
||||
item._custom = {
|
||||
barStart,
|
||||
barEnd,
|
||||
start: startValue,
|
||||
end: endValue,
|
||||
min,
|
||||
max,
|
||||
};
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
export class TimelineController extends BarController {
|
||||
static id = "timeline";
|
||||
|
||||
static defaults = {
|
||||
dataElementType: "textbar",
|
||||
dataElementOptions: ["text", "textColor", "textPadding"],
|
||||
elements: {
|
||||
showText: true,
|
||||
textPadding: 4,
|
||||
minBarWidth: 1,
|
||||
},
|
||||
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static overrides = {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
parseObjectData(meta, data, start, count) {
|
||||
const iScale = meta.iScale;
|
||||
const vScale = meta.vScale;
|
||||
const labels = iScale.getLabels();
|
||||
const singleScale = iScale === vScale;
|
||||
const parsed: any[] = [];
|
||||
let i;
|
||||
let ilen;
|
||||
let item;
|
||||
let entry;
|
||||
|
||||
for (i = start, ilen = start + count; i < ilen; ++i) {
|
||||
entry = data[i];
|
||||
item = {};
|
||||
item[iScale.axis] = singleScale || iScale.parse(labels[i], i);
|
||||
parsed.push(parseValue(entry, item, vScale, i));
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
getLabelAndValue(index) {
|
||||
const meta = this._cachedMeta;
|
||||
const { vScale } = meta;
|
||||
const data = this.getDataset().data[index] as TimeLineData;
|
||||
|
||||
return {
|
||||
label: vScale!.getLabelForValue(this.index) || "",
|
||||
value: data.label || "",
|
||||
};
|
||||
}
|
||||
|
||||
updateElements(
|
||||
bars: BarElement[],
|
||||
start: number,
|
||||
count: number,
|
||||
mode: "reset" | "resize" | "none" | "hide" | "show" | "default" | "active"
|
||||
) {
|
||||
const vScale = this._cachedMeta.vScale!;
|
||||
const iScale = this._cachedMeta.iScale!;
|
||||
const dataset = this.getDataset();
|
||||
|
||||
const firstOpts = this.resolveDataElementOptions(start, mode);
|
||||
const sharedOptions = this.getSharedOptions(firstOpts);
|
||||
const includeOptions = this.includeOptions(mode, sharedOptions!);
|
||||
|
||||
const horizontal = vScale.isHorizontal();
|
||||
|
||||
this.updateSharedOptions(sharedOptions!, mode, firstOpts);
|
||||
|
||||
for (let index = start; index < start + count; index++) {
|
||||
const data = dataset.data[index] as TimeLineData;
|
||||
|
||||
const y = vScale.getPixelForValue(this.index);
|
||||
|
||||
const xStart = iScale.getPixelForValue(
|
||||
Math.max(iScale.min, data.start.getTime())
|
||||
);
|
||||
const xEnd = iScale.getPixelForValue(data.end.getTime());
|
||||
const width = xEnd - xStart;
|
||||
|
||||
const parsed = this.getParsed(index);
|
||||
const stack = (parsed._stacks || {})[vScale.axis];
|
||||
|
||||
const height = 10;
|
||||
|
||||
const properties: TextBarProps = {
|
||||
horizontal,
|
||||
x: xStart + width / 2, // Center of the bar
|
||||
y: y - height, // Top of bar
|
||||
width,
|
||||
height: 0,
|
||||
base: y + height, // Bottom of bar,
|
||||
// Text
|
||||
text: data.label,
|
||||
};
|
||||
|
||||
if (includeOptions) {
|
||||
properties.options =
|
||||
sharedOptions || this.resolveDataElementOptions(index, mode);
|
||||
|
||||
properties.options = {
|
||||
...properties.options,
|
||||
backgroundColor: data.color,
|
||||
};
|
||||
}
|
||||
const options = properties.options || bars[index].options;
|
||||
|
||||
setBorderSkipped(properties, options, stack, index);
|
||||
setInflateAmount(properties, options, 1);
|
||||
this.updateElement(bars[index], index, properties as any, mode);
|
||||
}
|
||||
}
|
||||
|
||||
removeHoverStyle(_element, _datasetIndex, _index) {
|
||||
// this._setStyle(element, index, 'active', false);
|
||||
}
|
||||
|
||||
setHoverStyle(_element, _datasetIndex, _index) {
|
||||
// this._setStyle(element, index, 'active', true);
|
||||
}
|
||||
}
|
@@ -1,11 +1,11 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { getGraphColorByIndex } from "../../../common/color/colors";
|
||||
import { hex2rgb, lab2hex, rgb2lab } from "../../../common/color/convert-color";
|
||||
import { labBrighten } from "../../../common/color/lab";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stateColorProperties } from "../../../common/entity/state_color";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity";
|
||||
import { computeCssValue } from "../../../resources/css-variables";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { hex2rgb, lab2hex, rgb2lab } from "../../common/color/convert-color";
|
||||
import { labBrighten } from "../../common/color/lab";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { stateColorProperties } from "../../common/entity/state_color";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { computeCssValue } from "../../resources/css-variables";
|
||||
|
||||
const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
|
||||
media_player: {
|
@@ -33,9 +33,10 @@ export class HaAssistChip extends MdAssistChip {
|
||||
}
|
||||
/** Set the size of mdc icons **/
|
||||
::slotted([slot="icon"]),
|
||||
::slotted([slot="trailingIcon"]) {
|
||||
::slotted([slot="trailing-icon"]) {
|
||||
display: flex;
|
||||
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||
font-size: var(--_label-text-size) !important;
|
||||
}
|
||||
|
||||
.trailing.icon ::slotted(*),
|
||||
|
@@ -178,7 +178,7 @@ class HaEntityStatePicker extends LitElement {
|
||||
no-style
|
||||
@item-moved=${this._moveItem}
|
||||
.disabled=${this.disabled}
|
||||
filter="button.trailing.action"
|
||||
handle-selector="button.primary.action"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
@@ -195,12 +195,7 @@ class HaEntityStatePicker extends LitElement {
|
||||
.label=${label}
|
||||
selected
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDrag}
|
||||
data-handle
|
||||
></ha-svg-icon>
|
||||
|
||||
<ha-svg-icon slot="icon" .path=${mdiDrag}></ha-svg-icon>
|
||||
${label}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
|
@@ -276,6 +276,8 @@ export class HaAreaPicker extends LitElement {
|
||||
icon: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
temperature_entity_id: null,
|
||||
humidity_entity_id: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
@@ -294,6 +296,8 @@ export class HaAreaPicker extends LitElement {
|
||||
icon: "mdi:plus",
|
||||
aliases: [],
|
||||
labels: [],
|
||||
temperature_entity_id: null,
|
||||
humidity_entity_id: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
@@ -378,6 +382,8 @@ export class HaAreaPicker extends LitElement {
|
||||
picture: null,
|
||||
labels: [],
|
||||
aliases: [],
|
||||
temperature_entity_id: null,
|
||||
humidity_entity_id: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
@@ -396,6 +402,8 @@ export class HaAreaPicker extends LitElement {
|
||||
picture: null,
|
||||
labels: [],
|
||||
aliases: [],
|
||||
temperature_entity_id: null,
|
||||
humidity_entity_id: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
|
@@ -295,10 +295,12 @@ export class HaAssistChat extends LitElement {
|
||||
this._addMessage(userMessage);
|
||||
this.requestUpdate("_audioRecorder");
|
||||
|
||||
const hassMessage: AssistMessage = {
|
||||
let hassMessage = {
|
||||
who: "hass",
|
||||
text: "…",
|
||||
error: false,
|
||||
};
|
||||
let currentDeltaRole = "";
|
||||
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||
try {
|
||||
const unsub = await runAssistPipeline(
|
||||
@@ -328,6 +330,43 @@ export class HaAssistChat extends LitElement {
|
||||
this._addMessage(hassMessage);
|
||||
}
|
||||
|
||||
if (event.type === "intent-progress") {
|
||||
const delta = event.data.chat_log_delta;
|
||||
|
||||
// new message
|
||||
if (delta.role) {
|
||||
// If currentDeltaRole exists, it means we're receiving our
|
||||
// second or later message. Let's add it to the chat.
|
||||
if (currentDeltaRole && delta.role && hassMessage.text !== "…") {
|
||||
// Remove progress indicator of previous message
|
||||
hassMessage.text = hassMessage.text.substring(
|
||||
0,
|
||||
hassMessage.text.length - 1
|
||||
);
|
||||
|
||||
hassMessage = {
|
||||
who: "hass",
|
||||
text: "…",
|
||||
error: false,
|
||||
};
|
||||
this._addMessage(hassMessage);
|
||||
}
|
||||
currentDeltaRole = delta.role;
|
||||
}
|
||||
|
||||
if (
|
||||
currentDeltaRole === "assistant" &&
|
||||
"content" in delta &&
|
||||
delta.content
|
||||
) {
|
||||
hassMessage.text =
|
||||
hassMessage.text.substring(0, hassMessage.text.length - 1) +
|
||||
delta.content +
|
||||
"…";
|
||||
this.requestUpdate("_conversation");
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "intent-end") {
|
||||
this._conversationId = event.data.intent_output.conversation_id;
|
||||
const plain = event.data.intent_output.response.speech?.plain;
|
||||
@@ -435,28 +474,71 @@ export class HaAssistChat extends LitElement {
|
||||
this._processing = true;
|
||||
this._audio?.pause();
|
||||
this._addMessage({ who: "user", text });
|
||||
const message: AssistMessage = {
|
||||
let hassMessage = {
|
||||
who: "hass",
|
||||
text: "…",
|
||||
error: false,
|
||||
};
|
||||
let currentDeltaRole = "";
|
||||
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||
this._addMessage(message);
|
||||
this._addMessage(hassMessage);
|
||||
try {
|
||||
const unsub = await runAssistPipeline(
|
||||
this.hass,
|
||||
(event) => {
|
||||
if (event.type === "intent-progress") {
|
||||
const delta = event.data.chat_log_delta;
|
||||
|
||||
// new message and previous message has content
|
||||
if (delta.role) {
|
||||
// If currentDeltaRole exists, it means we're receiving our
|
||||
// second or later message. Let's add it to the chat.
|
||||
if (
|
||||
currentDeltaRole &&
|
||||
delta.role === "assistant" &&
|
||||
hassMessage.text !== "…"
|
||||
) {
|
||||
// Remove progress indicator of previous message
|
||||
hassMessage.text = hassMessage.text.substring(
|
||||
0,
|
||||
hassMessage.text.length - 1
|
||||
);
|
||||
|
||||
hassMessage = {
|
||||
who: "hass",
|
||||
text: "…",
|
||||
error: false,
|
||||
};
|
||||
this._addMessage(hassMessage);
|
||||
}
|
||||
currentDeltaRole = delta.role;
|
||||
}
|
||||
|
||||
if (
|
||||
currentDeltaRole === "assistant" &&
|
||||
"content" in delta &&
|
||||
delta.content
|
||||
) {
|
||||
hassMessage.text =
|
||||
hassMessage.text.substring(0, hassMessage.text.length - 1) +
|
||||
delta.content +
|
||||
"…";
|
||||
this.requestUpdate("_conversation");
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "intent-end") {
|
||||
this._conversationId = event.data.intent_output.conversation_id;
|
||||
const plain = event.data.intent_output.response.speech?.plain;
|
||||
if (plain) {
|
||||
message.text = plain.speech;
|
||||
hassMessage.text = plain.speech;
|
||||
}
|
||||
this.requestUpdate("_conversation");
|
||||
unsub();
|
||||
}
|
||||
if (event.type === "error") {
|
||||
message.text = event.data.message;
|
||||
message.error = true;
|
||||
hassMessage.text = event.data.message;
|
||||
hassMessage.error = true;
|
||||
this.requestUpdate("_conversation");
|
||||
unsub();
|
||||
}
|
||||
@@ -470,8 +552,8 @@ export class HaAssistChat extends LitElement {
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
message.text = this.hass.localize("ui.dialogs.voice_command.error");
|
||||
message.error = true;
|
||||
hassMessage.text = this.hass.localize("ui.dialogs.voice_command.error");
|
||||
hassMessage.error = true;
|
||||
this.requestUpdate("_conversation");
|
||||
} finally {
|
||||
this._processing = false;
|
||||
@@ -532,7 +614,7 @@ export class HaAssistChat extends LitElement {
|
||||
float: var(--float-end);
|
||||
text-align: right;
|
||||
border-bottom-right-radius: 0px;
|
||||
background-color: var(--primary-color);
|
||||
background-color: var(--chat-background-color-user, var(--primary-color));
|
||||
color: var(--text-primary-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
@@ -543,7 +625,10 @@ export class HaAssistChat extends LitElement {
|
||||
margin-inline-start: initial;
|
||||
float: var(--float-start);
|
||||
border-bottom-left-radius: 0px;
|
||||
background-color: var(--secondary-background-color);
|
||||
background-color: var(
|
||||
--chat-background-color-hass,
|
||||
var(--secondary-background-color)
|
||||
);
|
||||
|
||||
color: var(--primary-text-color);
|
||||
direction: var(--direction);
|
||||
|
@@ -329,14 +329,12 @@ export class HaBaseTimeInput extends LitElement {
|
||||
:host([clearable]) {
|
||||
position: relative;
|
||||
}
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.time-input-wrap-wrap {
|
||||
display: flex;
|
||||
}
|
||||
.time-input-wrap {
|
||||
display: flex;
|
||||
flex: var(--time-input-flex, unset);
|
||||
border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
@@ -345,6 +343,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
}
|
||||
ha-textfield {
|
||||
width: 55px;
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
--mdc-shape-small: 0;
|
||||
--text-field-appearance: none;
|
||||
|
@@ -23,6 +23,9 @@ export class HaButton extends Button {
|
||||
.slot-container {
|
||||
overflow: var(--button-slot-container-overflow, visible);
|
||||
}
|
||||
:host([destructive]) {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -9,12 +9,13 @@ import {
|
||||
endOfMonth,
|
||||
endOfWeek,
|
||||
endOfYear,
|
||||
isThisYear,
|
||||
startOfDay,
|
||||
startOfMonth,
|
||||
startOfWeek,
|
||||
startOfYear,
|
||||
isThisYear,
|
||||
} from "date-fns";
|
||||
import { fromZonedTime, toZonedTime } from "date-fns-tz";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -22,16 +23,18 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import { calcDate, shiftDateRange } from "../common/datetime/calc_date";
|
||||
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
|
||||
import {
|
||||
formatShortDateTimeWithYear,
|
||||
formatShortDateTime,
|
||||
formatShortDateTimeWithYear,
|
||||
} from "../common/datetime/format_date_time";
|
||||
import { useAmPm } from "../common/datetime/use_am_pm";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { TimeZone } from "../data/translation";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./date-range-picker";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-textarea";
|
||||
import "./ha-icon-button-next";
|
||||
import "./ha-icon-button-prev";
|
||||
import "./ha-textarea";
|
||||
|
||||
export type DateRangePickerRanges = Record<string, [Date, Date]>;
|
||||
|
||||
@@ -51,7 +54,7 @@ export class HaDateRangePicker extends LitElement {
|
||||
public autoApply = false;
|
||||
|
||||
@property({ attribute: "time-picker", type: Boolean })
|
||||
public timePicker = true;
|
||||
public timePicker = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@@ -197,14 +200,15 @@ export class HaDateRangePicker extends LitElement {
|
||||
?auto-apply=${this.autoApply}
|
||||
time-picker=${this.timePicker}
|
||||
twentyfour-hours=${this._hour24format}
|
||||
start-date=${this.startDate.toISOString()}
|
||||
end-date=${this.endDate.toISOString()}
|
||||
start-date=${this._formatDate(this.startDate)}
|
||||
end-date=${this._formatDate(this.endDate)}
|
||||
?ranges=${this.ranges !== false}
|
||||
opening-direction=${ifDefined(
|
||||
this.openingDirection || this._calcedOpeningDirection
|
||||
)}
|
||||
first-day=${firstWeekdayIndex(this.hass.locale)}
|
||||
language=${this.hass.locale.language}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
<div slot="input" class="date-range-inputs" @click=${this._handleClick}>
|
||||
${!this.minimal
|
||||
@@ -325,9 +329,31 @@ export class HaDateRangePicker extends LitElement {
|
||||
}
|
||||
|
||||
private _applyDateRange() {
|
||||
if (this.hass.locale.time_zone === TimeZone.server) {
|
||||
const dateRangePicker = this._dateRangePicker;
|
||||
|
||||
const startDate = fromZonedTime(
|
||||
dateRangePicker.start,
|
||||
this.hass.config.time_zone
|
||||
);
|
||||
const endDate = fromZonedTime(
|
||||
dateRangePicker.end,
|
||||
this.hass.config.time_zone
|
||||
);
|
||||
|
||||
dateRangePicker.clickRange([startDate, endDate]);
|
||||
}
|
||||
|
||||
this._dateRangePicker.clickedApply();
|
||||
}
|
||||
|
||||
private _formatDate(date: Date): string {
|
||||
if (this.hass.locale.time_zone === TimeZone.server) {
|
||||
return toZonedTime(date, this.hass.config.time_zone).toISOString();
|
||||
}
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
private get _dateRangePicker() {
|
||||
const dateRangePicker = this.shadowRoot!.querySelector(
|
||||
"date-range-picker"
|
||||
@@ -358,6 +384,16 @@ export class HaDateRangePicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const startDate = ev.detail.startDate;
|
||||
const endDate = ev.detail.endDate;
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { startDate, endDate },
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
||||
ha-icon-button {
|
||||
|
@@ -11,6 +11,7 @@ import "./ha-icon-button";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { bytesToString } from "../util/bytes-to-string";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -23,6 +24,8 @@ declare global {
|
||||
export class HaFileUpload extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@property() public accept!: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
@@ -31,6 +34,10 @@ export class HaFileUpload extends LitElement {
|
||||
|
||||
@property() public secondary?: string;
|
||||
|
||||
@property({ attribute: "uploading-label" }) public uploadingLabel?: string;
|
||||
|
||||
@property({ attribute: "delete-label" }) public deleteLabel?: string;
|
||||
|
||||
@property() public supports?: string;
|
||||
|
||||
@property({ type: Object }) public value?: File | File[] | FileList | string;
|
||||
@@ -73,23 +80,22 @@ export class HaFileUpload extends LitElement {
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const localize = this.localize || this.hass!.localize;
|
||||
return html`
|
||||
${this.uploading
|
||||
? html`<div class="container">
|
||||
<div class="uploading">
|
||||
<span class="header"
|
||||
>${this.value
|
||||
? this.hass?.localize(
|
||||
"ui.components.file-upload.uploading_name",
|
||||
{ name: this._name }
|
||||
)
|
||||
: this.hass?.localize(
|
||||
"ui.components.file-upload.uploading"
|
||||
)}</span
|
||||
>${this.uploadingLabel || this.value
|
||||
? localize("ui.components.file-upload.uploading_name", {
|
||||
name: this._name,
|
||||
})
|
||||
: localize("ui.components.file-upload.uploading")}</span
|
||||
>
|
||||
${this.progress
|
||||
? html`<div class="progress">
|
||||
${this.progress}${blankBeforePercent(this.hass!.locale)}%
|
||||
${this.progress}${this.hass &&
|
||||
blankBeforePercent(this.hass!.locale)}%
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -116,14 +122,11 @@ export class HaFileUpload extends LitElement {
|
||||
.path=${this.icon || mdiFileUpload}
|
||||
></ha-svg-icon>
|
||||
<ha-button unelevated @click=${this._openFilePicker}>
|
||||
${this.label ||
|
||||
this.hass?.localize("ui.components.file-upload.label")}
|
||||
${this.label || localize("ui.components.file-upload.label")}
|
||||
</ha-button>
|
||||
<span class="secondary"
|
||||
>${this.secondary ||
|
||||
this.hass?.localize(
|
||||
"ui.components.file-upload.secondary"
|
||||
)}</span
|
||||
localize("ui.components.file-upload.secondary")}</span
|
||||
>
|
||||
<span class="supports">${this.supports}</span>`
|
||||
: typeof this.value === "string"
|
||||
@@ -136,8 +139,7 @@ export class HaFileUpload extends LitElement {
|
||||
</div>
|
||||
<ha-icon-button
|
||||
@click=${this._clearValue}
|
||||
.label=${this.hass?.localize("ui.common.delete") ||
|
||||
"Delete"}
|
||||
.label=${this.deleteLabel || localize("ui.common.delete")}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
@@ -155,8 +157,8 @@ export class HaFileUpload extends LitElement {
|
||||
</div>
|
||||
<ha-icon-button
|
||||
@click=${this._clearValue}
|
||||
.label=${this.hass?.localize("ui.common.delete") ||
|
||||
"Delete"}
|
||||
.label=${this.deleteLabel ||
|
||||
localize("ui.common.delete")}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
@@ -238,6 +240,10 @@ export class HaFileUpload extends LitElement {
|
||||
border-radius: var(--mdc-shape-small, 4px);
|
||||
height: 100%;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
label.container {
|
||||
border: dashed 1px
|
||||
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
|
||||
|
@@ -79,6 +79,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
.disabled=${this.disabled}
|
||||
@opening=${this._handleOpen}
|
||||
@closing=${this._handleClose}
|
||||
positioning="fixed"
|
||||
>
|
||||
<ha-textfield
|
||||
slot="trigger"
|
||||
|
@@ -17,6 +17,7 @@ export class HaMdListItem extends MdListItem {
|
||||
}
|
||||
md-item {
|
||||
overflow: var(--md-item-overflow, hidden);
|
||||
align-items: var(--md-item-align-items, center);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { mdiDeleteOutline, mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyle } from "../resources/styles";
|
||||
@@ -8,6 +8,7 @@ import type { HomeAssistant } from "../types";
|
||||
import "./ha-button";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-textfield";
|
||||
import "./ha-input-helper-text";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@customElement("ha-multi-textfield")
|
||||
@@ -20,6 +21,8 @@ class HaMultiTextField extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public helper?: string;
|
||||
|
||||
@property({ attribute: false }) public inputType?: string;
|
||||
|
||||
@property({ attribute: false }) public inputSuffix?: string;
|
||||
@@ -69,12 +72,21 @@ class HaMultiTextField extends LitElement {
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
<div class="layout horizontal center-center">
|
||||
<div class="layout horizontal">
|
||||
<ha-button @click=${this._addItem} .disabled=${this.disabled}>
|
||||
${this.addLabel ?? this.hass?.localize("ui.common.add") ?? "Add"}
|
||||
${this.addLabel ??
|
||||
(this.label
|
||||
? this.hass?.localize("ui.components.multi-textfield.add_item", {
|
||||
item: this.label,
|
||||
})
|
||||
: this.hass?.localize("ui.common.add")) ??
|
||||
"Add"}
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-button>
|
||||
</div>
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -64,9 +64,13 @@ export class HaNetwork extends LitElement {
|
||||
>
|
||||
</ha-checkbox>
|
||||
</span>
|
||||
<span slot="heading" data-for="auto_configure"> Auto Configure </span>
|
||||
<span slot="heading" data-for="auto_configure">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.adapter.auto_configure"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description" data-for="auto_configure">
|
||||
Detected:
|
||||
${this.hass.localize("ui.panel.config.network.adapter.detected")}:
|
||||
${format_auto_detected_interfaces(this.networkConfig.adapters)}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
@@ -85,18 +89,21 @@ export class HaNetwork extends LitElement {
|
||||
</ha-checkbox>
|
||||
</span>
|
||||
<span slot="heading">
|
||||
Adapter: ${adapter.name}
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.adapter.adapter"
|
||||
)}:
|
||||
${adapter.name}
|
||||
${adapter.default
|
||||
? html`<ha-svg-icon .path=${mdiStar}></ha-svg-icon>
|
||||
(Default)`
|
||||
: ""}
|
||||
(${this.hass.localize("ui.common.default")})`
|
||||
: nothing}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${format_addresses([...adapter.ipv4, ...adapter.ipv6])}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
)
|
||||
: ""}
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -8,7 +8,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
// and "qr-scanner" defaults to a suboptimal implementation if it is not available.
|
||||
// The following import makes a better implementation available that is based on a
|
||||
// WebAssembly port of ZXing:
|
||||
import { setZXingModuleOverrides } from "barcode-detector";
|
||||
import { prepareZXingModule } from "barcode-detector";
|
||||
import type QrScanner from "qr-scanner";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
@@ -21,12 +21,14 @@ import "./ha-list-item";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
setZXingModuleOverrides({
|
||||
locateFile: (path: string, prefix: string) => {
|
||||
if (path.endsWith(".wasm")) {
|
||||
return "/static/js/zxing_reader.wasm";
|
||||
}
|
||||
return prefix + path;
|
||||
prepareZXingModule({
|
||||
overrides: {
|
||||
locateFile: (path: string, prefix: string) => {
|
||||
if (path.endsWith(".wasm")) {
|
||||
return "/static/js/zxing_reader.wasm";
|
||||
}
|
||||
return prefix + path;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -156,6 +156,7 @@ export class HaSelectSelector extends LitElement {
|
||||
no-style
|
||||
.disabled=${!this.selector.select.reorder}
|
||||
@item-moved=${this._itemMoved}
|
||||
handle-selector="button.primary.action"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
@@ -177,7 +178,6 @@ export class HaSelectSelector extends LitElement {
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDrag}
|
||||
data-handle
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
|
@@ -50,6 +50,7 @@ export class HaTextSelector extends LitElement {
|
||||
.inputType=${this.selector.text?.type}
|
||||
.inputSuffix=${this.selector.text?.suffix}
|
||||
.inputPrefix=${this.selector.text?.prefix}
|
||||
.helper=${this.helper}
|
||||
.autocomplete=${this.selector.text?.autocomplete}
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
|
@@ -1115,6 +1115,8 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
.child .play:not(.can_expand) {
|
||||
--mdc-icon-button-size: 70px;
|
||||
--mdc-icon-size: 48px;
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
ha-card:hover .image {
|
||||
@@ -1126,10 +1128,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
ha-card:hover .play:not(.can_expand) {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
ha-card:hover .play.can_expand {
|
||||
bottom: 8px;
|
||||
}
|
||||
@@ -1144,10 +1142,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
opacity 0.1s ease-out;
|
||||
}
|
||||
|
||||
.child .play:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.child .title {
|
||||
font-size: 16px;
|
||||
padding-top: 16px;
|
||||
@@ -1331,11 +1325,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
ha-browse-media-tts {
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
ha-card:hover .play:not(.can_expand) {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { css, html, LitElement, svg } from "lit";
|
||||
import { css, html, LitElement, nothing, svg } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { BRANCH_HEIGHT, SPACING } from "./hat-graph-const";
|
||||
@@ -41,8 +41,8 @@ export class HatGraphBranch extends LitElement {
|
||||
branches.push({
|
||||
x: width / 2 + total_width,
|
||||
height,
|
||||
start: c.hasAttribute("graphStart"),
|
||||
end: c.hasAttribute("graphEnd"),
|
||||
start: c.hasAttribute("graph-start"),
|
||||
end: c.hasAttribute("graph-end"),
|
||||
track: c.hasAttribute("track"),
|
||||
});
|
||||
total_width += width;
|
||||
@@ -65,11 +65,8 @@ export class HatGraphBranch extends LitElement {
|
||||
return html`
|
||||
<slot name="head"></slot>
|
||||
${!this.start
|
||||
? svg`
|
||||
<svg
|
||||
id="top"
|
||||
width="${this._totalWidth}"
|
||||
>
|
||||
? html`
|
||||
<svg id="top" width=${this._totalWidth}>
|
||||
${this._branches.map((branch) =>
|
||||
branch.start
|
||||
? ""
|
||||
@@ -86,7 +83,7 @@ export class HatGraphBranch extends LitElement {
|
||||
)}
|
||||
</svg>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
<div id="branches">
|
||||
<svg id="lines" width=${this._totalWidth} height=${this._maxHeight}>
|
||||
${this._branches.map((branch) => {
|
||||
@@ -107,11 +104,8 @@ export class HatGraphBranch extends LitElement {
|
||||
</div>
|
||||
|
||||
${!this.short
|
||||
? svg`
|
||||
<svg
|
||||
id="bottom"
|
||||
width="${this._totalWidth}"
|
||||
>
|
||||
? html`
|
||||
<svg id="bottom" width=${this._totalWidth}>
|
||||
${this._branches.map((branch) => {
|
||||
if (branch.end) return "";
|
||||
return svg`
|
||||
@@ -128,7 +122,7 @@ export class HatGraphBranch extends LitElement {
|
||||
})}
|
||||
</svg>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -7,13 +7,15 @@ import type { RegistryEntry } from "./registry";
|
||||
export { subscribeAreaRegistry } from "./ws-area_registry";
|
||||
|
||||
export interface AreaRegistryEntry extends RegistryEntry {
|
||||
aliases: string[];
|
||||
area_id: string;
|
||||
floor_id: string | null;
|
||||
name: string;
|
||||
picture: string | null;
|
||||
humidity_entity_id: string | null;
|
||||
icon: string | null;
|
||||
labels: string[];
|
||||
aliases: string[];
|
||||
name: string;
|
||||
picture: string | null;
|
||||
temperature_entity_id: string | null;
|
||||
}
|
||||
|
||||
export type AreaEntityLookup = Record<string, EntityRegistryEntry[]>;
|
||||
@@ -21,12 +23,14 @@ export type AreaEntityLookup = Record<string, EntityRegistryEntry[]>;
|
||||
export type AreaDeviceLookup = Record<string, DeviceRegistryEntry[]>;
|
||||
|
||||
export interface AreaRegistryEntryMutableParams {
|
||||
name: string;
|
||||
floor_id?: string | null;
|
||||
picture?: string | null;
|
||||
icon?: string | null;
|
||||
aliases?: string[];
|
||||
floor_id?: string | null;
|
||||
humidity_entity_id?: string | null;
|
||||
icon?: string | null;
|
||||
labels?: string[];
|
||||
name: string;
|
||||
picture?: string | null;
|
||||
temperature_entity_id?: string | null;
|
||||
}
|
||||
|
||||
export const createAreaRegistryEntry = (
|
||||
|
@@ -108,6 +108,34 @@ interface PipelineIntentStartEvent extends PipelineEventBase {
|
||||
intent_input: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ConversationChatLogAssistantDelta {
|
||||
role: "assistant";
|
||||
content: string;
|
||||
tool_calls: {
|
||||
id: string;
|
||||
tool_name: string;
|
||||
tool_args: Record<string, unknown>;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface ConversationChatLogToolResultDelta {
|
||||
role: "tool_result";
|
||||
agent_id: string;
|
||||
tool_call_id: string;
|
||||
tool_name: string;
|
||||
tool_result: unknown;
|
||||
}
|
||||
interface PipelineIntentProgressEvent extends PipelineEventBase {
|
||||
type: "intent-progress";
|
||||
data: {
|
||||
chat_log_delta:
|
||||
| Partial<ConversationChatLogAssistantDelta>
|
||||
// These always come in 1 chunk
|
||||
| ConversationChatLogToolResultDelta;
|
||||
};
|
||||
}
|
||||
|
||||
interface PipelineIntentEndEvent extends PipelineEventBase {
|
||||
type: "intent-end";
|
||||
data: {
|
||||
@@ -141,6 +169,7 @@ export type PipelineRunEvent =
|
||||
| PipelineSTTStartEvent
|
||||
| PipelineSTTEndEvent
|
||||
| PipelineIntentStartEvent
|
||||
| PipelineIntentProgressEvent
|
||||
| PipelineIntentEndEvent
|
||||
| PipelineTTSStartEvent
|
||||
| PipelineTTSEndEvent;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { memoize } from "@fullcalendar/core/internal";
|
||||
import { setHours, setMinutes } from "date-fns";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -11,22 +12,35 @@ import type { HomeAssistant } from "../types";
|
||||
import { fileDownload } from "../util/file_download";
|
||||
import { domainToName } from "./integration";
|
||||
import type { FrontendLocaleData } from "./translation";
|
||||
import checkValidDate from "../common/datetime/check_valid_date";
|
||||
import { handleFetchPromise } from "../util/hass-call-api";
|
||||
|
||||
export const enum BackupScheduleState {
|
||||
export const enum BackupScheduleRecurrence {
|
||||
NEVER = "never",
|
||||
DAILY = "daily",
|
||||
MONDAY = "mon",
|
||||
TUESDAY = "tue",
|
||||
WEDNESDAY = "wed",
|
||||
THURSDAY = "thu",
|
||||
FRIDAY = "fri",
|
||||
SATURDAY = "sat",
|
||||
SUNDAY = "sun",
|
||||
CUSTOM_DAYS = "custom_days",
|
||||
}
|
||||
|
||||
export type BackupDay = "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun";
|
||||
|
||||
export const BACKUP_DAYS: BackupDay[] = [
|
||||
"mon",
|
||||
"tue",
|
||||
"wed",
|
||||
"thu",
|
||||
"fri",
|
||||
"sat",
|
||||
"sun",
|
||||
];
|
||||
|
||||
export const sortWeekdays = (weekdays) =>
|
||||
weekdays.sort((a, b) => BACKUP_DAYS.indexOf(a) - BACKUP_DAYS.indexOf(b));
|
||||
|
||||
export interface BackupConfig {
|
||||
last_attempted_automatic_backup: string | null;
|
||||
last_completed_automatic_backup: string | null;
|
||||
next_automatic_backup: string | null;
|
||||
next_automatic_backup_additional?: boolean;
|
||||
create_backup: {
|
||||
agent_ids: string[];
|
||||
include_addons: string[] | null;
|
||||
@@ -41,8 +55,11 @@ export interface BackupConfig {
|
||||
days?: number | null;
|
||||
};
|
||||
schedule: {
|
||||
state: BackupScheduleState;
|
||||
recurrence: BackupScheduleRecurrence;
|
||||
time?: string | null;
|
||||
days: BackupDay[];
|
||||
};
|
||||
agents: BackupAgentsConfig;
|
||||
}
|
||||
|
||||
export interface BackupMutableConfig {
|
||||
@@ -59,21 +76,39 @@ export interface BackupMutableConfig {
|
||||
copies?: number | null;
|
||||
days?: number | null;
|
||||
};
|
||||
schedule?: BackupScheduleState;
|
||||
schedule?: {
|
||||
recurrence: BackupScheduleRecurrence;
|
||||
time?: string | null;
|
||||
days?: BackupDay[] | null;
|
||||
};
|
||||
agents?: BackupAgentsConfig;
|
||||
}
|
||||
|
||||
export type BackupAgentsConfig = Record<string, BackupAgentConfig>;
|
||||
|
||||
export interface BackupAgentConfig {
|
||||
protected: boolean;
|
||||
}
|
||||
|
||||
export interface BackupAgent {
|
||||
agent_id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface BackupContentAgent {
|
||||
size: number;
|
||||
protected: boolean;
|
||||
}
|
||||
|
||||
export interface BackupContent {
|
||||
backup_id: string;
|
||||
date: string;
|
||||
name: string;
|
||||
protected: boolean;
|
||||
size: number;
|
||||
agent_ids?: string[];
|
||||
agents: Record<string, BackupContentAgent>;
|
||||
failed_agent_ids?: string[];
|
||||
extra_metadata?: {
|
||||
"supervisor.addon_update"?: string;
|
||||
};
|
||||
with_automatic_settings: boolean;
|
||||
}
|
||||
|
||||
@@ -135,8 +170,12 @@ export const updateBackupConfig = (
|
||||
config: BackupMutableConfig
|
||||
) => hass.callWS({ type: "backup/config/update", ...config });
|
||||
|
||||
export const getBackupDownloadUrl = (id: string, agentId: string) =>
|
||||
`/api/backup/download/${id}?agent_id=${agentId}`;
|
||||
export const getBackupDownloadUrl = (
|
||||
id: string,
|
||||
agentId: string,
|
||||
password?: string | null
|
||||
) =>
|
||||
`/api/backup/download/${id}?agent_id=${agentId}${password ? `&password=${password}` : ""}`;
|
||||
|
||||
export const fetchBackupInfo = (hass: HomeAssistant): Promise<BackupInfo> =>
|
||||
hass.callWS({
|
||||
@@ -193,27 +232,23 @@ export const restoreBackup = (
|
||||
export const uploadBackup = async (
|
||||
hass: HomeAssistant,
|
||||
file: File,
|
||||
agent_ids: string[]
|
||||
): Promise<void> => {
|
||||
agentIds: string[]
|
||||
): Promise<{ backup_id: string }> => {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
|
||||
const params = agent_ids.reduce((acc, agent_id) => {
|
||||
acc.append("agent_id", agent_id);
|
||||
return acc;
|
||||
}, new URLSearchParams());
|
||||
const params = new URLSearchParams();
|
||||
|
||||
const resp = await hass.fetchWithAuth(
|
||||
`/api/backup/upload?${params.toString()}`,
|
||||
{
|
||||
agentIds.forEach((agentId) => {
|
||||
params.append("agent_id", agentId);
|
||||
});
|
||||
|
||||
return handleFetchPromise(
|
||||
hass.fetchWithAuth(`/api/backup/upload?${params.toString()}`, {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (!resp.ok) {
|
||||
throw new Error(`${resp.status} ${resp.statusText}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getPreferredAgentForDownload = (agents: string[]) => {
|
||||
@@ -229,6 +264,19 @@ export const getPreferredAgentForDownload = (agents: string[]) => {
|
||||
return agents[0];
|
||||
};
|
||||
|
||||
export const canDecryptBackupOnDownload = (
|
||||
hass: HomeAssistant,
|
||||
backup_id: string,
|
||||
agent_id: string,
|
||||
password: string
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "backup/can_decrypt_on_download",
|
||||
backup_id,
|
||||
agent_id,
|
||||
password,
|
||||
});
|
||||
|
||||
export const CORE_LOCAL_AGENT = "backup.local";
|
||||
export const HASSIO_LOCAL_AGENT = "hassio.local";
|
||||
export const CLOUD_AGENT = "cloud.cloud";
|
||||
@@ -244,13 +292,18 @@ export const isNetworkMountAgent = (agentId: string) => {
|
||||
export const computeBackupAgentName = (
|
||||
localize: LocalizeFunc,
|
||||
agentId: string,
|
||||
agentIds?: string[]
|
||||
agents: BackupAgent[]
|
||||
) => {
|
||||
if (isLocalAgent(agentId)) {
|
||||
return localize("ui.panel.config.backup.agents.local_agent");
|
||||
}
|
||||
const [domain, name] = agentId.split(".");
|
||||
|
||||
const agent = agents.find((a) => a.agent_id === agentId);
|
||||
|
||||
const domain = agentId.split(".")[0];
|
||||
const name = agent ? agent.name : agentId.split(".")[1];
|
||||
|
||||
// If it's a network mount agent, only show the name
|
||||
if (isNetworkMountAgent(agentId)) {
|
||||
return name;
|
||||
}
|
||||
@@ -258,13 +311,38 @@ export const computeBackupAgentName = (
|
||||
const domainName = domainToName(localize, domain);
|
||||
|
||||
// If there are multiple agents for a domain, show the name
|
||||
const showName = agentIds
|
||||
? agentIds.filter((a) => a.split(".")[0] === domain).length > 1
|
||||
: true;
|
||||
const showName =
|
||||
agents.filter((a) => a.agent_id.split(".")[0] === domain).length > 1;
|
||||
|
||||
return showName ? `${domainName}: ${name}` : domainName;
|
||||
};
|
||||
|
||||
export const computeBackupSize = (backup: BackupContent) =>
|
||||
Math.max(...Object.values(backup.agents).map((agent) => agent.size));
|
||||
|
||||
export type BackupType = "automatic" | "manual" | "addon_update";
|
||||
|
||||
const BACKUP_TYPE_ORDER: BackupType[] = ["automatic", "manual", "addon_update"];
|
||||
|
||||
export const getBackupTypes = memoize((isHassio: boolean) =>
|
||||
isHassio
|
||||
? BACKUP_TYPE_ORDER
|
||||
: BACKUP_TYPE_ORDER.filter((type) => type !== "addon_update")
|
||||
);
|
||||
|
||||
export const computeBackupType = (
|
||||
backup: BackupContent,
|
||||
isHassio: boolean
|
||||
): BackupType => {
|
||||
if (backup.with_automatic_settings) {
|
||||
return "automatic";
|
||||
}
|
||||
if (isHassio && backup.extra_metadata?.["supervisor.addon_update"] != null) {
|
||||
return "addon_update";
|
||||
}
|
||||
return "manual";
|
||||
};
|
||||
|
||||
export const compareAgents = (a: string, b: string) => {
|
||||
const isLocalA = isLocalAgent(a);
|
||||
const isLocalB = isLocalAgent(b);
|
||||
@@ -337,9 +415,44 @@ export const downloadEmergencyKit = (
|
||||
geneateEmergencyKitFileName(hass, appendFileName)
|
||||
);
|
||||
|
||||
export const DEFAULT_OPTIMIZED_BACKUP_START_TIME = setMinutes(
|
||||
setHours(new Date(), 4),
|
||||
45
|
||||
);
|
||||
|
||||
export const DEFAULT_OPTIMIZED_BACKUP_END_TIME = setMinutes(
|
||||
setHours(new Date(), 5),
|
||||
45
|
||||
);
|
||||
|
||||
export const getFormattedBackupTime = memoizeOne(
|
||||
(locale: FrontendLocaleData, config: HassConfig) => {
|
||||
const date = setMinutes(setHours(new Date(), 4), 45);
|
||||
return formatTime(date, locale, config);
|
||||
(
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
backupTime?: Date | string | null
|
||||
) => {
|
||||
if (checkValidDate(backupTime as Date)) {
|
||||
return formatTime(backupTime as Date, locale, config);
|
||||
}
|
||||
if (typeof backupTime === "string" && backupTime) {
|
||||
const splitted = backupTime.split(":");
|
||||
const date = setMinutes(
|
||||
setHours(new Date(), parseInt(splitted[0])),
|
||||
parseInt(splitted[1])
|
||||
);
|
||||
return formatTime(date, locale, config);
|
||||
}
|
||||
|
||||
return `${formatTime(DEFAULT_OPTIMIZED_BACKUP_START_TIME, locale, config)} - ${formatTime(DEFAULT_OPTIMIZED_BACKUP_END_TIME, locale, config)}`;
|
||||
}
|
||||
);
|
||||
|
||||
export const SUPPORTED_UPLOAD_FORMAT = "application/x-tar";
|
||||
|
||||
export interface BackupUploadFileFormData {
|
||||
file?: File;
|
||||
}
|
||||
|
||||
export const INITIAL_UPLOAD_FORM_DATA: BackupUploadFileFormData = {
|
||||
file: undefined,
|
||||
};
|
||||
|
66
src/data/backup_onboarding.ts
Normal file
66
src/data/backup_onboarding.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { handleFetchPromise } from "../util/hass-call-api";
|
||||
import type { BackupContentExtended } from "./backup";
|
||||
import type {
|
||||
BackupManagerState,
|
||||
RestoreBackupStage,
|
||||
RestoreBackupState,
|
||||
} from "./backup_manager";
|
||||
|
||||
export interface BackupOnboardingInfo {
|
||||
state: BackupManagerState;
|
||||
last_non_idle_event?: {
|
||||
manager_state: BackupManagerState;
|
||||
stage: RestoreBackupStage | null;
|
||||
state: RestoreBackupState;
|
||||
reason: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface BackupOnboardingConfig extends BackupOnboardingInfo {
|
||||
backups: BackupContentExtended[];
|
||||
}
|
||||
|
||||
export const fetchBackupOnboardingInfo = async () =>
|
||||
handleFetchPromise<BackupOnboardingConfig>(
|
||||
fetch("/api/onboarding/backup/info")
|
||||
);
|
||||
|
||||
export interface RestoreOnboardingBackupParams {
|
||||
backup_id: string;
|
||||
agent_id: string;
|
||||
password?: string;
|
||||
restore_addons?: string[];
|
||||
restore_database?: boolean;
|
||||
restore_folders?: string[];
|
||||
}
|
||||
|
||||
export const restoreOnboardingBackup = async (
|
||||
params: RestoreOnboardingBackupParams
|
||||
) =>
|
||||
handleFetchPromise(
|
||||
fetch("/api/onboarding/backup/restore", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
);
|
||||
|
||||
export const uploadOnboardingBackup = async (
|
||||
file: File,
|
||||
agentIds: string[]
|
||||
): Promise<{ backup_id: string }> => {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
agentIds.forEach((agentId) => {
|
||||
params.append("agent_id", agentId);
|
||||
});
|
||||
|
||||
return handleFetchPromise(
|
||||
fetch(`/api/onboarding/backup/upload?${params.toString()}`, {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
})
|
||||
);
|
||||
};
|
167
src/data/bluetooth.ts
Normal file
167
src/data/bluetooth.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import {
|
||||
createCollection,
|
||||
type Connection,
|
||||
type UnsubscribeFunc,
|
||||
} from "home-assistant-js-websocket";
|
||||
import type { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import type { DataTableRowData } from "../components/data-table/ha-data-table";
|
||||
|
||||
export interface BluetoothDeviceData extends DataTableRowData {
|
||||
address: string;
|
||||
connectable: boolean;
|
||||
manufacturer_data: Record<number, string>;
|
||||
name: string;
|
||||
rssi: number;
|
||||
service_data: Record<string, string>;
|
||||
service_uuids: string[];
|
||||
source: string;
|
||||
time: number;
|
||||
tx_power: number;
|
||||
}
|
||||
|
||||
export interface BluetoothScannerDetails {
|
||||
source: string;
|
||||
connectable: boolean;
|
||||
name: string;
|
||||
adapter: string;
|
||||
}
|
||||
|
||||
export type BluetoothScannersDetails = Record<string, BluetoothScannerDetails>;
|
||||
|
||||
interface BluetoothRemoveDeviceData {
|
||||
address: string;
|
||||
}
|
||||
|
||||
interface BluetoothAdvertisementSubscriptionMessage {
|
||||
add?: BluetoothDeviceData[];
|
||||
change?: BluetoothDeviceData[];
|
||||
remove?: BluetoothRemoveDeviceData[];
|
||||
}
|
||||
|
||||
interface BluetoothScannersDetailsSubscriptionMessage {
|
||||
add?: BluetoothScannerDetails[];
|
||||
remove?: BluetoothScannerDetails[];
|
||||
}
|
||||
|
||||
export interface BluetoothAllocationsData {
|
||||
source: string;
|
||||
slots: number;
|
||||
free: number;
|
||||
allocated: string[];
|
||||
}
|
||||
|
||||
export const subscribeBluetoothScannersDetailsUpdates = (
|
||||
conn: Connection,
|
||||
store: Store<BluetoothScannersDetails>
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
conn.subscribeMessage<BluetoothScannersDetailsSubscriptionMessage>(
|
||||
(event) => {
|
||||
const data = { ...(store.state || {}) };
|
||||
if (event.add) {
|
||||
for (const device_data of event.add) {
|
||||
data[device_data.source] = device_data;
|
||||
}
|
||||
}
|
||||
if (event.remove) {
|
||||
for (const device_data of event.remove) {
|
||||
delete data[device_data.source];
|
||||
}
|
||||
}
|
||||
store.setState(data, true);
|
||||
},
|
||||
{
|
||||
type: `bluetooth/subscribe_scanner_details`,
|
||||
}
|
||||
);
|
||||
|
||||
export const subscribeBluetoothScannersDetails = (
|
||||
conn: Connection,
|
||||
callbackFunction: (bluetoothScannersDetails: BluetoothScannersDetails) => void
|
||||
) =>
|
||||
createCollection<BluetoothScannersDetails>(
|
||||
"_bluetoothScannerDetails",
|
||||
() => Promise.resolve<BluetoothScannersDetails>({}), // empty hash as initial state
|
||||
|
||||
subscribeBluetoothScannersDetailsUpdates,
|
||||
conn,
|
||||
callbackFunction
|
||||
);
|
||||
|
||||
const subscribeBluetoothAdvertisementsUpdates = (
|
||||
conn: Connection,
|
||||
store: Store<BluetoothDeviceData[]>
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
conn.subscribeMessage<BluetoothAdvertisementSubscriptionMessage>(
|
||||
(event) => {
|
||||
const data = [...(store.state || [])];
|
||||
if (event.add) {
|
||||
for (const device_data of event.add) {
|
||||
const index = data.findIndex(
|
||||
(d) => d.address === device_data.address
|
||||
);
|
||||
if (index === -1) {
|
||||
data.push(device_data);
|
||||
} else {
|
||||
data[index] = device_data;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (event.change) {
|
||||
for (const device_data of event.change) {
|
||||
const index = data.findIndex(
|
||||
(d) => d.address === device_data.address
|
||||
);
|
||||
if (index !== -1) {
|
||||
data[index] = device_data;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (event.remove) {
|
||||
for (const device_data of event.remove) {
|
||||
const index = data.findIndex(
|
||||
(d) => d.address === device_data.address
|
||||
);
|
||||
if (index !== -1) {
|
||||
data.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
store.setState(data, true);
|
||||
},
|
||||
{
|
||||
type: `bluetooth/subscribe_advertisements`,
|
||||
}
|
||||
);
|
||||
|
||||
export const subscribeBluetoothAdvertisements = (
|
||||
conn: Connection,
|
||||
callbackFunction: (bluetoothDeviceData: BluetoothDeviceData[]) => void
|
||||
) =>
|
||||
createCollection<BluetoothDeviceData[]>(
|
||||
"_bluetoothDeviceRows",
|
||||
() => Promise.resolve<BluetoothDeviceData[]>([]), // empty array as initial state
|
||||
|
||||
subscribeBluetoothAdvertisementsUpdates,
|
||||
conn,
|
||||
callbackFunction
|
||||
);
|
||||
|
||||
export const subscribeBluetoothConnectionAllocations = (
|
||||
conn: Connection,
|
||||
callbackFunction: (
|
||||
bluetoothAllocationsData: BluetoothAllocationsData[]
|
||||
) => void,
|
||||
configEntryId?: string
|
||||
): Promise<() => Promise<void>> => {
|
||||
const params: { type: string; config_entry_id?: string } = {
|
||||
type: "bluetooth/subscribe_connection_allocations",
|
||||
};
|
||||
if (configEntryId) {
|
||||
params.config_entry_id = configEntryId;
|
||||
}
|
||||
return conn.subscribeMessage<BluetoothAllocationsData[]>(
|
||||
(bluetoothAllocationsData) => callbackFunction(bluetoothAllocationsData),
|
||||
params
|
||||
);
|
||||
};
|
@@ -181,3 +181,6 @@ export const updateCloudGoogleEntityConfig = (
|
||||
|
||||
export const cloudSyncGoogleAssistant = (hass: HomeAssistant) =>
|
||||
hass.callApi("POST", "cloud/google_actions/sync");
|
||||
|
||||
export const fetchSupportPackage = (hass: HomeAssistant) =>
|
||||
hass.callApi<string>("GET", "cloud/support_package");
|
||||
|
@@ -19,6 +19,8 @@ export interface ConfigEntry {
|
||||
supports_remove_device: boolean;
|
||||
supports_unload: boolean;
|
||||
supports_reconfigure: boolean;
|
||||
supported_subentry_types: Record<string, { supports_reconfigure: boolean }>;
|
||||
num_subentries: number;
|
||||
pref_disable_new_entities: boolean;
|
||||
pref_disable_polling: boolean;
|
||||
disabled_by: "user" | null;
|
||||
@@ -27,6 +29,30 @@ export interface ConfigEntry {
|
||||
error_reason_translation_placeholders: Record<string, string> | null;
|
||||
}
|
||||
|
||||
export interface SubEntry {
|
||||
subentry_id: string;
|
||||
subentry_type: string;
|
||||
title: string;
|
||||
unique_id: string;
|
||||
}
|
||||
|
||||
export const getSubEntries = (hass: HomeAssistant, entry_id: string) =>
|
||||
hass.callWS<SubEntry[]>({
|
||||
type: "config_entries/subentries/list",
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const deleteSubEntry = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
subentry_id: string
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "config_entries/subentries/delete",
|
||||
entry_id,
|
||||
subentry_id,
|
||||
});
|
||||
|
||||
export type ConfigEntryMutableParams = Partial<
|
||||
Pick<
|
||||
ConfigEntry,
|
||||
|
@@ -2,7 +2,11 @@ import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { HaFormSchema } from "../components/ha-form/types";
|
||||
import type { ConfigEntry } from "./config_entries";
|
||||
|
||||
export type FlowType = "config_flow" | "options_flow" | "repair_flow";
|
||||
export type FlowType =
|
||||
| "config_flow"
|
||||
| "config_subentries_flow"
|
||||
| "options_flow"
|
||||
| "repair_flow";
|
||||
|
||||
export interface DataEntryFlowProgressedEvent {
|
||||
type: "data_entry_flow_progressed";
|
||||
|
@@ -17,6 +17,7 @@ export {
|
||||
export interface DeviceRegistryEntry extends RegistryEntry {
|
||||
id: string;
|
||||
config_entries: string[];
|
||||
config_entries_subentries: Record<string, (string | null)[]>;
|
||||
connections: [string, string][];
|
||||
identifiers: [string, string][];
|
||||
manufacturer: string | null;
|
||||
|
@@ -50,6 +50,7 @@ export interface EntityRegistryEntry extends RegistryEntry {
|
||||
icon: string | null;
|
||||
platform: string;
|
||||
config_entry_id: string | null;
|
||||
config_subentry_id: string | null;
|
||||
device_id: string | null;
|
||||
area_id: string | null;
|
||||
labels: string[];
|
||||
|
@@ -313,21 +313,34 @@ export const installHassioAddon = async (
|
||||
|
||||
export const updateHassioAddon = async (
|
||||
hass: HomeAssistant,
|
||||
slug: string
|
||||
slug: string,
|
||||
backup: boolean
|
||||
): Promise<void> => {
|
||||
if (atLeastVersion(hass.config.version, 2025, 2, 0)) {
|
||||
await hass.callWS({
|
||||
type: "hassio/update/addon",
|
||||
addon: slug,
|
||||
backup: backup,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
await hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/store/addons/${slug}/update`,
|
||||
method: "post",
|
||||
timeout: null,
|
||||
data: { backup },
|
||||
});
|
||||
} else {
|
||||
await hass.callApi<HassioResponse<void>>(
|
||||
"POST",
|
||||
`hassio/addons/${slug}/update`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await hass.callApi<HassioResponse<void>>(
|
||||
"POST",
|
||||
`hassio/addons/${slug}/update`,
|
||||
{ backup }
|
||||
);
|
||||
};
|
||||
|
||||
export const restartHassioAddon = async (
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { atLeastVersion } from "../../common/config/version";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { handleFetchPromise } from "../../util/hass-call-api";
|
||||
import type { HassioResponse } from "./common";
|
||||
import { hassioApiResultExtractor } from "./common";
|
||||
|
||||
@@ -82,34 +81,24 @@ export const fetchHassioBackups = async (
|
||||
};
|
||||
|
||||
export const fetchHassioBackupInfo = async (
|
||||
hass: HomeAssistant | undefined,
|
||||
hass: HomeAssistant,
|
||||
backup: string
|
||||
): Promise<HassioBackupDetail> => {
|
||||
if (hass) {
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
return hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/${
|
||||
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||
}/${backup}/info`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<HassioBackupDetail>>(
|
||||
"GET",
|
||||
`hassio/${
|
||||
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||
}/${backup}/info`
|
||||
)
|
||||
);
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
return hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/${
|
||||
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||
}/${backup}/info`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
// When called from onboarding we don't have hass
|
||||
return hassioApiResultExtractor(
|
||||
await handleFetchPromise(
|
||||
fetch(`/api/hassio/backups/${backup}/info`, {
|
||||
method: "GET",
|
||||
})
|
||||
await hass.callApi<HassioResponse<HassioBackupDetail>>(
|
||||
"GET",
|
||||
`hassio/${
|
||||
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||
}/${backup}/info`
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -240,24 +229,15 @@ export const uploadBackup = async (
|
||||
};
|
||||
|
||||
export const restoreBackup = async (
|
||||
hass: HomeAssistant | undefined,
|
||||
hass: HomeAssistant,
|
||||
type: HassioBackupDetail["type"],
|
||||
backupSlug: string,
|
||||
backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams,
|
||||
useSnapshotUrl: boolean
|
||||
): Promise<void> => {
|
||||
if (hass) {
|
||||
await hass.callApi<HassioResponse<{ job_id: string }>>(
|
||||
"POST",
|
||||
`hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
|
||||
backupDetails
|
||||
);
|
||||
} else {
|
||||
await handleFetchPromise(
|
||||
fetch(`/api/hassio/backups/${backupSlug}/restore/${type}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(backupDetails),
|
||||
})
|
||||
);
|
||||
}
|
||||
await hass.callApi<HassioResponse<{ job_id: string }>>(
|
||||
"POST",
|
||||
`hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
|
||||
backupDetails
|
||||
);
|
||||
};
|
||||
|
@@ -5,6 +5,7 @@ import type { HomeAssistant } from "../types";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
|
||||
export const integrationsWithPanel = {
|
||||
bluetooth: "config/bluetooth",
|
||||
matter: "config/matter",
|
||||
mqtt: "config/mqtt",
|
||||
thread: "config/thread",
|
||||
|
@@ -2,6 +2,8 @@ import type { HomeAssistant } from "../types";
|
||||
|
||||
export const SENSOR_DEVICE_CLASS_BATTERY = "battery";
|
||||
export const SENSOR_DEVICE_CLASS_TIMESTAMP = "timestamp";
|
||||
export const SENSOR_DEVICE_CLASS_TEMPERATURE = "temperature";
|
||||
export const SENSOR_DEVICE_CLASS_HUMIDITY = "humidity";
|
||||
|
||||
export interface SensorDeviceClassUnits {
|
||||
units: string[];
|
||||
|
46
src/data/sub_config_flow.ts
Normal file
46
src/data/sub_config_flow.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { DataEntryFlowStep } from "./data_entry_flow";
|
||||
|
||||
const HEADERS = {
|
||||
"HA-Frontend-Base": `${location.protocol}//${location.host}`,
|
||||
};
|
||||
|
||||
export const createSubConfigFlow = (
|
||||
hass: HomeAssistant,
|
||||
configEntryId: string,
|
||||
subFlowType: string,
|
||||
subentry_id?: string
|
||||
) =>
|
||||
hass.callApi<DataEntryFlowStep>(
|
||||
"POST",
|
||||
"config/config_entries/subentries/flow",
|
||||
{
|
||||
handler: [configEntryId, subFlowType],
|
||||
show_advanced_options: Boolean(hass.userData?.showAdvanced),
|
||||
subentry_id,
|
||||
},
|
||||
HEADERS
|
||||
);
|
||||
|
||||
export const fetchSubConfigFlow = (hass: HomeAssistant, flowId: string) =>
|
||||
hass.callApi<DataEntryFlowStep>(
|
||||
"GET",
|
||||
`config/config_entries/subentries/flow/${flowId}`,
|
||||
undefined,
|
||||
HEADERS
|
||||
);
|
||||
|
||||
export const handleSubConfigFlowStep = (
|
||||
hass: HomeAssistant,
|
||||
flowId: string,
|
||||
data: Record<string, any>
|
||||
) =>
|
||||
hass.callApi<DataEntryFlowStep>(
|
||||
"POST",
|
||||
`config/config_entries/subentries/flow/${flowId}`,
|
||||
data,
|
||||
HEADERS
|
||||
);
|
||||
|
||||
export const deleteSubConfigFlow = (hass: HomeAssistant, flowId: string) =>
|
||||
hass.callApi("DELETE", `config/config_entries/subentries/flow/${flowId}`);
|
@@ -6,15 +6,27 @@ export const restartCore = async (hass: HomeAssistant) => {
|
||||
await hass.callService("homeassistant", "restart");
|
||||
};
|
||||
|
||||
export const updateCore = async (hass: HomeAssistant) => {
|
||||
export const updateCore = async (hass: HomeAssistant, backup: boolean) => {
|
||||
if (atLeastVersion(hass.config.version, 2025, 2, 0)) {
|
||||
await hass.callWS({
|
||||
type: "hassio/update/core",
|
||||
backup: backup,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
await hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/core/update",
|
||||
method: "post",
|
||||
timeout: null,
|
||||
data: { backup },
|
||||
});
|
||||
} else {
|
||||
await hass.callApi<HassioResponse<void>>("POST", "hassio/core/update");
|
||||
return;
|
||||
}
|
||||
|
||||
await hass.callApi<HassioResponse<void>>("POST", "hassio/core/update", {
|
||||
backup,
|
||||
});
|
||||
};
|
||||
|
@@ -63,6 +63,7 @@ export type TranslationCategory =
|
||||
| "entity_component"
|
||||
| "exceptions"
|
||||
| "config"
|
||||
| "config_subentries"
|
||||
| "config_panel"
|
||||
| "options"
|
||||
| "device_automation"
|
||||
|
@@ -13,6 +13,7 @@ import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { showToast } from "../util/toast";
|
||||
import type { EntitySources } from "./entity_sources";
|
||||
|
||||
export enum UpdateEntityFeature {
|
||||
INSTALL = 1,
|
||||
@@ -60,6 +61,10 @@ export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
|
||||
entity_id: entityId,
|
||||
});
|
||||
|
||||
const HOME_ASSISTANT_CORE_TITLE = "Home Assistant Core";
|
||||
const HOME_ASSISTANT_SUPERVISOR_TITLE = "Home Assistant Supervisor";
|
||||
const HOME_ASSISTANT_OS_TITLE = "Home Assistant Operating System";
|
||||
|
||||
export const filterUpdateEntities = (
|
||||
entities: HassEntities,
|
||||
language?: string
|
||||
@@ -69,22 +74,22 @@ export const filterUpdateEntities = (
|
||||
(entity) => computeStateDomain(entity) === "update"
|
||||
) as UpdateEntity[]
|
||||
).sort((a, b) => {
|
||||
if (a.attributes.title === "Home Assistant Core") {
|
||||
if (a.attributes.title === HOME_ASSISTANT_CORE_TITLE) {
|
||||
return -3;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Core") {
|
||||
if (b.attributes.title === HOME_ASSISTANT_CORE_TITLE) {
|
||||
return 3;
|
||||
}
|
||||
if (a.attributes.title === "Home Assistant Operating System") {
|
||||
if (a.attributes.title === HOME_ASSISTANT_OS_TITLE) {
|
||||
return -2;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Operating System") {
|
||||
if (b.attributes.title === HOME_ASSISTANT_OS_TITLE) {
|
||||
return 2;
|
||||
}
|
||||
if (a.attributes.title === "Home Assistant Supervisor") {
|
||||
if (a.attributes.title === HOME_ASSISTANT_SUPERVISOR_TITLE) {
|
||||
return -1;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Supervisor") {
|
||||
if (b.attributes.title === HOME_ASSISTANT_SUPERVISOR_TITLE) {
|
||||
return 1;
|
||||
}
|
||||
return caseInsensitiveStringCompare(
|
||||
@@ -201,3 +206,32 @@ export const computeUpdateStateDisplay = (
|
||||
|
||||
return hass.formatEntityState(stateObj);
|
||||
};
|
||||
|
||||
type UpdateType = "addon" | "home_assistant" | "generic";
|
||||
|
||||
export const getUpdateType = (
|
||||
stateObj: UpdateEntity,
|
||||
entitySources: EntitySources
|
||||
): UpdateType => {
|
||||
const entity_id = stateObj.entity_id;
|
||||
const domain = entitySources[entity_id]?.domain;
|
||||
if (domain !== "hassio") {
|
||||
return "generic";
|
||||
}
|
||||
|
||||
const title = stateObj.attributes.title || "";
|
||||
if (title === HOME_ASSISTANT_CORE_TITLE) {
|
||||
return "home_assistant";
|
||||
}
|
||||
|
||||
if (
|
||||
![
|
||||
HOME_ASSISTANT_CORE_TITLE,
|
||||
HOME_ASSISTANT_SUPERVISOR_TITLE,
|
||||
HOME_ASSISTANT_OS_TITLE,
|
||||
].includes(title)
|
||||
) {
|
||||
return "addon";
|
||||
}
|
||||
return "generic";
|
||||
};
|
||||
|
@@ -1,5 +1,11 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { createCollection } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant, ThemeSettings } from "../types";
|
||||
import {
|
||||
fetchFrontendUserData,
|
||||
saveFrontendUserData,
|
||||
subscribeFrontendUserData,
|
||||
} from "./frontend";
|
||||
|
||||
export interface ThemeVars {
|
||||
// Incomplete
|
||||
@@ -50,3 +56,16 @@ export const subscribeThemes = (
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
||||
export const SELECTED_THEME_KEY = "selectedTheme";
|
||||
|
||||
export const saveSelectedTheme = (hass: HomeAssistant, data?: ThemeSettings) =>
|
||||
saveFrontendUserData(hass.connection, SELECTED_THEME_KEY, data);
|
||||
|
||||
export const subscribeSelectedTheme = (
|
||||
hass: HomeAssistant,
|
||||
callback: (selectedTheme?: ThemeSettings | null) => void
|
||||
) => subscribeFrontendUserData(hass.connection, SELECTED_THEME_KEY, callback);
|
||||
|
||||
export const fetchSelectedTheme = (hass: HomeAssistant) =>
|
||||
fetchFrontendUserData(hass.connection, SELECTED_THEME_KEY);
|
||||
|
@@ -282,6 +282,8 @@ class DataEntryFlowDialog extends LitElement {
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
.navigateToResult=${this._params
|
||||
.navigateToResult}
|
||||
></step-flow-create-entry>
|
||||
`}
|
||||
`}
|
||||
@@ -312,32 +314,31 @@ class DataEntryFlowDialog extends LitElement {
|
||||
private async _processStep(
|
||||
step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep>
|
||||
): Promise<void> {
|
||||
if (step instanceof Promise) {
|
||||
this._loading = "loading_step";
|
||||
try {
|
||||
this._step = await step;
|
||||
} catch (err: any) {
|
||||
this.closeDialog();
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error"
|
||||
),
|
||||
text: err?.body?.message,
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
this._loading = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (step === undefined) {
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
this._loading = "loading_step";
|
||||
let _step: DataEntryFlowStep;
|
||||
try {
|
||||
_step = await step;
|
||||
} catch (err: any) {
|
||||
this.closeDialog();
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error"
|
||||
),
|
||||
text: err?.body?.message,
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
this._loading = undefined;
|
||||
}
|
||||
|
||||
this._step = undefined;
|
||||
await this.updateComplete;
|
||||
this._step = step;
|
||||
this._step = _step;
|
||||
}
|
||||
|
||||
private async _subscribeDataEntryFlowProgressed() {
|
||||
|
@@ -77,7 +77,7 @@ export class FlowPreviewGeneric extends LitElement {
|
||||
(await this._unsub)();
|
||||
this._unsub = undefined;
|
||||
}
|
||||
if (this.flowType === "repair_flow") {
|
||||
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
@@ -147,7 +147,7 @@ class FlowPreviewTemplate extends LitElement {
|
||||
(await this._unsub)();
|
||||
this._unsub = undefined;
|
||||
}
|
||||
if (this.flowType === "repair_flow") {
|
||||
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
@@ -16,7 +16,9 @@ export const loadConfigFlowDialog = loadDataEntryFlowDialog;
|
||||
|
||||
export const showConfigFlowDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig">
|
||||
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig"> & {
|
||||
entryId?: string;
|
||||
}
|
||||
): void =>
|
||||
showFlowDialog(element, dialogParams, {
|
||||
flowType: "config_flow",
|
||||
|
@@ -148,8 +148,8 @@ export interface DataEntryFlowDialogParams {
|
||||
}) => void;
|
||||
flowConfig: FlowConfig;
|
||||
showAdvanced?: boolean;
|
||||
entryId?: string;
|
||||
dialogParentElement?: HTMLElement;
|
||||
navigateToResult?: boolean;
|
||||
}
|
||||
|
||||
export const loadDataEntryFlowDialog = () => import("./dialog-data-entry-flow");
|
||||
|
275
src/dialogs/config-flow/show-dialog-sub-config-flow.ts
Normal file
275
src/dialogs/config-flow/show-dialog-sub-config-flow.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import { html } from "lit";
|
||||
import type { ConfigEntry } from "../../data/config_entries";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import {
|
||||
createSubConfigFlow,
|
||||
deleteSubConfigFlow,
|
||||
fetchSubConfigFlow,
|
||||
handleSubConfigFlowStep,
|
||||
} from "../../data/sub_config_flow";
|
||||
import type { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
|
||||
import {
|
||||
loadDataEntryFlowDialog,
|
||||
showFlowDialog,
|
||||
} from "./show-dialog-data-entry-flow";
|
||||
|
||||
export const loadSubConfigFlowDialog = loadDataEntryFlowDialog;
|
||||
|
||||
export const showSubConfigFlowDialog = (
|
||||
element: HTMLElement,
|
||||
configEntry: ConfigEntry,
|
||||
flowType: string,
|
||||
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig"> & {
|
||||
subEntryId?: string;
|
||||
}
|
||||
): void =>
|
||||
showFlowDialog(element, dialogParams, {
|
||||
flowType: "config_subentries_flow",
|
||||
showDevices: true,
|
||||
createFlow: async (hass, handler) => {
|
||||
const [step] = await Promise.all([
|
||||
createSubConfigFlow(hass, handler, flowType, dialogParams.subEntryId),
|
||||
hass.loadFragmentTranslation("config"),
|
||||
hass.loadBackendTranslation("config_subentries", configEntry.domain),
|
||||
hass.loadBackendTranslation("selector", configEntry.domain),
|
||||
// Used as fallback if no header defined for step
|
||||
hass.loadBackendTranslation("title", configEntry.domain),
|
||||
]);
|
||||
return step;
|
||||
},
|
||||
fetchFlow: async (hass, flowId) => {
|
||||
const step = await fetchSubConfigFlow(hass, flowId);
|
||||
await hass.loadFragmentTranslation("config");
|
||||
await hass.loadBackendTranslation(
|
||||
"config_subentries",
|
||||
configEntry.domain
|
||||
);
|
||||
await hass.loadBackendTranslation("selector", configEntry.domain);
|
||||
return step;
|
||||
},
|
||||
handleFlowStep: handleSubConfigFlowStep,
|
||||
deleteFlow: deleteSubConfigFlow,
|
||||
|
||||
renderAbortDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.abort.${step.reason}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||
`
|
||||
: step.reason;
|
||||
},
|
||||
|
||||
renderShowFormStepHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`,
|
||||
step.description_placeholders
|
||||
) || hass.localize(`component.${configEntry.domain}.title`)
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||
`
|
||||
: "";
|
||||
},
|
||||
|
||||
renderShowFormStepFieldLabel(hass, step, field, options) {
|
||||
if (field.type === "expandable") {
|
||||
return hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.name`
|
||||
);
|
||||
}
|
||||
|
||||
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.${prefix}data.${field.name}`
|
||||
) || field.name
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepFieldHelper(hass, step, field, options) {
|
||||
if (field.type === "expandable") {
|
||||
return hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.description`
|
||||
);
|
||||
}
|
||||
|
||||
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.${prefix}data_description.${field.name}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
|
||||
return description
|
||||
? html`<ha-markdown breaks .content=${description}></ha-markdown>`
|
||||
: "";
|
||||
},
|
||||
|
||||
renderShowFormStepFieldError(hass, step, error) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${step.translation_domain || step.translation_domain || configEntry.domain}.config_subentries.${flowType}.error.${error}`,
|
||||
step.description_placeholders
|
||||
) || error
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepFieldLocalizeValue(hass, _step, key) {
|
||||
return hass.localize(`component.${configEntry.domain}.selector.${key}`);
|
||||
},
|
||||
|
||||
renderShowFormStepSubmitButton(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.submit`
|
||||
) ||
|
||||
hass.localize(
|
||||
`ui.panel.config.integrations.config_flow.${
|
||||
step.last_step === false ? "next" : "submit"
|
||||
}`
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
renderExternalStepHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`
|
||||
) ||
|
||||
hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.external_step.open_site"
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
renderExternalStepDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
|
||||
step.description_placeholders
|
||||
);
|
||||
|
||||
return html`
|
||||
<p>
|
||||
${hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.external_step.description"
|
||||
)}
|
||||
</p>
|
||||
${description
|
||||
? html`
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
breaks
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
},
|
||||
|
||||
renderCreateEntryDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.create_entry.${
|
||||
step.description || "default"
|
||||
}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
|
||||
return html`
|
||||
${description
|
||||
? html`
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
breaks
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
`
|
||||
: ""}
|
||||
<p>
|
||||
${hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.created_config",
|
||||
{ name: step.title }
|
||||
)}
|
||||
</p>
|
||||
`;
|
||||
},
|
||||
|
||||
renderShowFormProgressHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`
|
||||
) || hass.localize(`component.${configEntry.domain}.title`)
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormProgressDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.progress.${step.progress_action}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||
`
|
||||
: "";
|
||||
},
|
||||
|
||||
renderMenuHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`,
|
||||
step.description_placeholders
|
||||
) || hass.localize(`component.${configEntry.domain}.title`)
|
||||
);
|
||||
},
|
||||
|
||||
renderMenuDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||
`
|
||||
: "";
|
||||
},
|
||||
|
||||
renderMenuOption(hass, step, option) {
|
||||
return hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.menu_options.${option}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
},
|
||||
|
||||
renderLoadingDescription(hass, reason, handler, step) {
|
||||
if (reason !== "loading_flow" && reason !== "loading_step") {
|
||||
return "";
|
||||
}
|
||||
const domain = step?.handler || handler;
|
||||
return hass.localize(
|
||||
`ui.panel.config.integrations.config_flow.loading.${reason}`,
|
||||
{
|
||||
integration: domain
|
||||
? domainToName(hass.localize, domain)
|
||||
: // when we are continuing a config flow, we only know the ID and not the domain
|
||||
hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.loading.fallback_title"
|
||||
),
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
@@ -60,6 +60,7 @@ class StepFlowAbort extends LitElement {
|
||||
dialogClosedCallback: this.params.dialogClosedCallback,
|
||||
startFlowHandler: this.domain,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
navigateToResult: this.params.navigateToResult,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@@ -19,6 +19,7 @@ import { showAlertDialog } from "../generic/show-dialog-box";
|
||||
import { showVoiceAssistantSetupDialog } from "../voice-assistant-setup/show-voice-assistant-setup-dialog";
|
||||
import type { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
import { navigate } from "../../common/navigate";
|
||||
|
||||
@customElement("step-flow-create-entry")
|
||||
class StepFlowCreateEntry extends LitElement {
|
||||
@@ -28,6 +29,8 @@ class StepFlowCreateEntry extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
|
||||
|
||||
navigateToResult = false;
|
||||
|
||||
private _devices = memoizeOne(
|
||||
(
|
||||
showDevices: boolean,
|
||||
@@ -65,7 +68,8 @@ class StepFlowCreateEntry extends LitElement {
|
||||
|
||||
if (
|
||||
devices.length !== 1 ||
|
||||
devices[0].primary_config_entry !== this.step.result?.entry_id
|
||||
devices[0].primary_config_entry !== this.step.result?.entry_id ||
|
||||
this.step.result.domain === "voip"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -151,6 +155,11 @@ class StepFlowCreateEntry extends LitElement {
|
||||
|
||||
private _flowDone(): void {
|
||||
fireEvent(this, "flow-update", { step: undefined });
|
||||
if (this.step.result && this.navigateToResult) {
|
||||
navigate(
|
||||
`/config/integrations/integration/${this.step.result.domain}#config_entry=${this.step.result.entry_id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async _areaPicked(ev: CustomEvent) {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { mdiAlertOutline } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-md-dialog";
|
||||
@@ -117,9 +116,7 @@ class DialogBox extends LitElement {
|
||||
@click=${this._confirm}
|
||||
?dialogInitialFocus=${!this._params.prompt &&
|
||||
!this._params.destructive}
|
||||
class=${classMap({
|
||||
destructive: this._params.destructive || false,
|
||||
})}
|
||||
?destructive=${this._params.destructive}
|
||||
>
|
||||
${this._params.confirmText
|
||||
? this._params.confirmText
|
||||
@@ -187,9 +184,6 @@ class DialogBox extends LitElement {
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.destructive {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
ha-textfield {
|
||||
width: 100%;
|
||||
}
|
||||
|
@@ -33,6 +33,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
|
||||
"switch",
|
||||
"valve",
|
||||
"water_heater",
|
||||
"weather",
|
||||
];
|
||||
/** Domains with full height more info dialog */
|
||||
export const DOMAINS_FULL_HEIGHT_MORE_INFO = ["update"];
|
||||
|
@@ -2,6 +2,7 @@ import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { BINARY_STATE_OFF } from "../../../common/const";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
@@ -10,10 +11,18 @@ import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-faded";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import type { BackupConfig } from "../../../data/backup";
|
||||
import { fetchBackupConfig } from "../../../data/backup";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import type { EntitySources } from "../../../data/entity_sources";
|
||||
import { fetchEntitySourcesWithCache } from "../../../data/entity_sources";
|
||||
import type { UpdateEntity } from "../../../data/update";
|
||||
import {
|
||||
getUpdateType,
|
||||
UpdateEntityFeature,
|
||||
updateIsInstalling,
|
||||
updateReleaseNotes,
|
||||
@@ -33,6 +42,103 @@ class MoreInfoUpdate extends LitElement {
|
||||
|
||||
@state() private _markdownLoading = true;
|
||||
|
||||
@state() private _backupConfig?: BackupConfig;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
private async _fetchBackupConfig() {
|
||||
const { config } = await fetchBackupConfig(this.hass);
|
||||
this._backupConfig = config;
|
||||
}
|
||||
|
||||
private async _fetchEntitySources() {
|
||||
this._entitySources = await fetchEntitySourcesWithCache(this.hass);
|
||||
}
|
||||
|
||||
private _computeCreateBackupTexts():
|
||||
| { title: string; description?: string }
|
||||
| undefined {
|
||||
if (
|
||||
!this.stateObj ||
|
||||
!supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const updateType = this._entitySources
|
||||
? getUpdateType(this.stateObj, this._entitySources)
|
||||
: "generic";
|
||||
|
||||
// Automatic or manual for Home Assistant update
|
||||
if (updateType === "home_assistant") {
|
||||
const isBackupConfigValid =
|
||||
!!this._backupConfig &&
|
||||
!!this._backupConfig.create_backup.password &&
|
||||
this._backupConfig.create_backup.agent_ids.length > 0;
|
||||
|
||||
if (!isBackupConfigValid) {
|
||||
return {
|
||||
title: this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.manual"
|
||||
),
|
||||
description: this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.manual_description"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const lastAutomaticBackupDate = this._backupConfig
|
||||
?.last_completed_automatic_backup
|
||||
? new Date(this._backupConfig?.last_completed_automatic_backup)
|
||||
: null;
|
||||
const now = new Date();
|
||||
|
||||
return {
|
||||
title: this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.automatic"
|
||||
),
|
||||
description: lastAutomaticBackupDate
|
||||
? this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.automatic_description_last",
|
||||
{
|
||||
relative_time: relativeTime(
|
||||
lastAutomaticBackupDate,
|
||||
this.hass.locale,
|
||||
now,
|
||||
true
|
||||
),
|
||||
}
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.automatic_description_none"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Addon backup
|
||||
if (updateType === "addon") {
|
||||
const version = this.stateObj.attributes.installed_version;
|
||||
return {
|
||||
title: this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.addon"
|
||||
),
|
||||
description: version
|
||||
? this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.addon_description",
|
||||
{ version: version }
|
||||
)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback to generic UI
|
||||
return {
|
||||
title: this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.generic"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
!this.hass ||
|
||||
@@ -47,6 +153,8 @@ class MoreInfoUpdate extends LitElement {
|
||||
this.stateObj.attributes.skipped_version ===
|
||||
this.stateObj.attributes.latest_version;
|
||||
|
||||
const createBackupTexts = this._computeCreateBackupTexts();
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<div class="summary">
|
||||
@@ -133,6 +241,27 @@ class MoreInfoUpdate extends LitElement {
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="footer">
|
||||
${createBackupTexts
|
||||
? html`
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">${createBackupTexts.title}</span>
|
||||
${createBackupTexts.description
|
||||
? html`
|
||||
<span slot="supporting-text">
|
||||
${createBackupTexts.description}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
<ha-switch
|
||||
slot="end"
|
||||
id="create-backup"
|
||||
.disabled=${updateIsInstalling(this.stateObj)}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
`
|
||||
: nothing}
|
||||
<div class="actions">
|
||||
${this.stateObj.state === BINARY_STATE_OFF &&
|
||||
this.stateObj.attributes.skipped_version
|
||||
@@ -186,6 +315,14 @@ class MoreInfoUpdate extends LitElement {
|
||||
if (supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES)) {
|
||||
this._fetchReleaseNotes();
|
||||
}
|
||||
if (supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
|
||||
this._fetchEntitySources().then(() => {
|
||||
const type = getUpdateType(this.stateObj!, this._entitySources!);
|
||||
if (type === "home_assistant") {
|
||||
this._fetchBackupConfig();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _markdownLoaded() {
|
||||
@@ -205,11 +342,28 @@ class MoreInfoUpdate extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
get _shouldCreateBackup(): boolean {
|
||||
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
|
||||
return false;
|
||||
}
|
||||
const createBackupSwitch = this.shadowRoot?.getElementById(
|
||||
"create-backup"
|
||||
) as HaSwitch;
|
||||
if (createBackupSwitch) {
|
||||
return createBackupSwitch.checked;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _handleInstall(): void {
|
||||
const installData: Record<string, any> = {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
};
|
||||
|
||||
if (this._shouldCreateBackup) {
|
||||
installData.backup = true;
|
||||
}
|
||||
|
||||
if (
|
||||
supportsFeature(this.stateObj!, UpdateEntityFeature.SPECIFIC_VERSION) &&
|
||||
this.stateObj!.attributes.latest_version
|
||||
@@ -289,12 +443,20 @@ class MoreInfoUpdate extends LitElement {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
ha-settings-row {
|
||||
ha-md-list {
|
||||
width: 100%;
|
||||
padding: 0 24px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: -16px;
|
||||
margin-top: -4px;
|
||||
--md-sys-color-surface: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
}
|
||||
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 24px;
|
||||
--md-list-item-trailing-space: 24px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
@@ -1,18 +1,13 @@
|
||||
import "@material/mwc-tab";
|
||||
import "@material/mwc-tab-bar";
|
||||
import {
|
||||
mdiEye,
|
||||
mdiGauge,
|
||||
mdiThermometer,
|
||||
mdiWaterPercent,
|
||||
mdiWeatherWindy,
|
||||
} from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { mdiEye, mdiGauge, mdiWaterPercent, mdiWeatherWindy } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { formatDateWeekdayDay } from "../../../common/datetime/format_date";
|
||||
import { formatTimeWeekday } from "../../../common/datetime/format_time";
|
||||
import { formatDateWeekdayShort } from "../../../common/datetime/format_date";
|
||||
import { formatTime } from "../../../common/datetime/format_time";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type {
|
||||
ForecastEvent,
|
||||
@@ -23,11 +18,16 @@ import {
|
||||
getDefaultForecastType,
|
||||
getForecast,
|
||||
getSupportedForecastTypes,
|
||||
getSecondaryWeatherAttribute,
|
||||
getWeatherStateIcon,
|
||||
getWeatherUnit,
|
||||
getWind,
|
||||
subscribeForecast,
|
||||
weatherIcons,
|
||||
weatherSVGStyles,
|
||||
} from "../../../data/weather";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-relative-time";
|
||||
import "../../../components/ha-state-icon";
|
||||
|
||||
@customElement("more-info-weather")
|
||||
class MoreInfoWeather extends LitElement {
|
||||
@@ -137,23 +137,90 @@ class MoreInfoWeather extends LitElement {
|
||||
const hourly = forecastData?.type === "hourly";
|
||||
const dayNight = forecastData?.type === "twice_daily";
|
||||
|
||||
const weatherStateIcon = getWeatherStateIcon(this.stateObj.state, this);
|
||||
|
||||
return html`
|
||||
${this._showValue(this.stateObj.attributes.temperature)
|
||||
? html`
|
||||
<div class="flex">
|
||||
<ha-svg-icon .path=${mdiThermometer}></ha-svg-icon>
|
||||
<div class="main">
|
||||
${this.hass.localize("ui.card.weather.attributes.temperature")}
|
||||
</div>
|
||||
<div>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"temperature"
|
||||
)}
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="icon-image">
|
||||
${weatherStateIcon ||
|
||||
html`
|
||||
<ha-state-icon
|
||||
class="weather-icon"
|
||||
.stateObj=${this.stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
`}
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="name-state">
|
||||
<div class="state">
|
||||
${this.hass.formatEntityState(this.stateObj)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="time-ago">
|
||||
<ha-relative-time
|
||||
id="last_changed"
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
<simple-tooltip animation-delay="0" for="last_changed">
|
||||
<div>
|
||||
<div class="row">
|
||||
<span class="column-name">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.last_changed"
|
||||
)}:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.last_updated"
|
||||
)}:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_updated}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
</div>
|
||||
</div>
|
||||
</simple-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="temp-attribute">
|
||||
<div class="temp">
|
||||
${this.stateObj.attributes.temperature !== undefined &&
|
||||
this.stateObj.attributes.temperature !== null
|
||||
? html`
|
||||
${formatNumber(
|
||||
this.stateObj.attributes.temperature,
|
||||
this.hass.locale
|
||||
)} <span
|
||||
>${getWeatherUnit(
|
||||
this.hass.config,
|
||||
this.stateObj,
|
||||
"temperature"
|
||||
)}</span
|
||||
>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="attribute">
|
||||
${getSecondaryWeatherAttribute(
|
||||
this.hass,
|
||||
this.stateObj,
|
||||
forecast!
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${this._showValue(this.stateObj.attributes.pressure)
|
||||
? html`
|
||||
<div class="flex">
|
||||
@@ -169,7 +236,7 @@ class MoreInfoWeather extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
${this._showValue(this.stateObj.attributes.humidity)
|
||||
? html`
|
||||
<div class="flex">
|
||||
@@ -185,7 +252,7 @@ class MoreInfoWeather extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
${this._showValue(this.stateObj.attributes.wind_speed)
|
||||
? html`
|
||||
<div class="flex">
|
||||
@@ -203,7 +270,7 @@ class MoreInfoWeather extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
${this._showValue(this.stateObj.attributes.visibility)
|
||||
? html`
|
||||
<div class="flex">
|
||||
@@ -219,7 +286,7 @@ class MoreInfoWeather extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
${forecast
|
||||
? html`
|
||||
<div class="section">
|
||||
@@ -242,76 +309,90 @@ class MoreInfoWeather extends LitElement {
|
||||
)}
|
||||
</mwc-tab-bar>`
|
||||
: nothing}
|
||||
${forecast.map((item) =>
|
||||
this._showValue(item.templow) || this._showValue(item.temperature)
|
||||
? html`<div class="flex">
|
||||
${item.condition
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${weatherIcons[item.condition]}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
<div class="main">
|
||||
${dayNight
|
||||
? html`
|
||||
${formatDateWeekdayDay(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
(${item.is_daytime !== false
|
||||
? this.hass!.localize("ui.card.weather.day")
|
||||
: this.hass!.localize("ui.card.weather.night")})
|
||||
`
|
||||
: hourly
|
||||
<div class="forecast">
|
||||
${forecast.map((item) =>
|
||||
this._showValue(item.templow) ||
|
||||
this._showValue(item.temperature)
|
||||
? html`
|
||||
<div>
|
||||
<div>
|
||||
${dayNight
|
||||
? html`
|
||||
${formatDateWeekdayShort(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
<div class="daynight">
|
||||
${item.is_daytime !== false
|
||||
? this.hass!.localize("ui.card.weather.day")
|
||||
: this.hass!.localize(
|
||||
"ui.card.weather.night"
|
||||
)}<br />
|
||||
</div>
|
||||
`
|
||||
: hourly
|
||||
? html`
|
||||
${formatTime(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
`
|
||||
: html`
|
||||
${formatDateWeekdayShort(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
`}
|
||||
</div>
|
||||
${this._showValue(item.condition)
|
||||
? html`
|
||||
${formatTimeWeekday(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
<div class="forecast-image-icon">
|
||||
${getWeatherStateIcon(
|
||||
item.condition!,
|
||||
this,
|
||||
!(
|
||||
item.is_daytime ||
|
||||
item.is_daytime === undefined
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
${formatDateWeekdayDay(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
`}
|
||||
</div>
|
||||
<div class="templow">
|
||||
${this._showValue(item.templow)
|
||||
? this.hass.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
"templow",
|
||||
item.templow
|
||||
)
|
||||
: hourly
|
||||
? ""
|
||||
: "—"}
|
||||
</div>
|
||||
<div class="temp">
|
||||
${this._showValue(item.temperature)
|
||||
? this.hass.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
"temperature",
|
||||
item.temperature
|
||||
)
|
||||
: "—"}
|
||||
</div>
|
||||
</div>`
|
||||
: ""
|
||||
)}
|
||||
: nothing}
|
||||
<div class="temp">
|
||||
${this._showValue(item.temperature)
|
||||
? html`${formatNumber(
|
||||
item.temperature,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: "—"}
|
||||
</div>
|
||||
<div class="templow">
|
||||
${this._showValue(item.templow)
|
||||
? html`${formatNumber(
|
||||
item.templow!,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: hourly
|
||||
? nothing
|
||||
: "—"}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
${this.stateObj.attributes.attribution
|
||||
? html`
|
||||
<div class="attribution">
|
||||
${this.stateObj.attributes.attribution}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -321,56 +402,186 @@ class MoreInfoWeather extends LitElement {
|
||||
];
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-svg-icon {
|
||||
color: var(--paper-item-icon-color);
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
weatherSVGStyles,
|
||||
css`
|
||||
ha-svg-icon {
|
||||
color: var(--paper-item-icon-color);
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
|
||||
mwc-tab-bar {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
mwc-tab-bar {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: 16px 0 8px 0;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.section {
|
||||
margin: 16px 0 8px 0;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
}
|
||||
.flex > div:last-child {
|
||||
direction: ltr;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
}
|
||||
.flex > div:last-child {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
margin-left: 24px;
|
||||
margin-inline-start: 24px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.main {
|
||||
flex: 1;
|
||||
margin-left: 24px;
|
||||
margin-inline-start: 24px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
|
||||
.temp,
|
||||
.templow {
|
||||
min-width: 48px;
|
||||
text-align: right;
|
||||
direction: ltr;
|
||||
}
|
||||
.attribution {
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.templow {
|
||||
margin: 0 16px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.time-ago,
|
||||
.attribute {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.attribution {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
.attribution,
|
||||
.templow,
|
||||
.daynight,
|
||||
.attribute,
|
||||
.time-ago {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.icon-image {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 64px;
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
|
||||
.icon-image > * {
|
||||
flex: 0 0 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.weather-icon {
|
||||
--mdc-icon-size: 64px;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.temp-attribute {
|
||||
text-align: var(--float-end);
|
||||
}
|
||||
|
||||
.temp-attribute .temp {
|
||||
position: relative;
|
||||
margin-right: 24px;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.temp-attribute .temp span {
|
||||
position: absolute;
|
||||
font-size: 24px;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.state,
|
||||
.temp-attribute .temp {
|
||||
font-size: 28px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.attribute {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.name-state {
|
||||
overflow: hidden;
|
||||
padding-right: 12px;
|
||||
padding-inline-end: 12px;
|
||||
padding-inline-start: initial;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.state {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.forecast {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 16px;
|
||||
padding-bottom: 0px;
|
||||
overflow-x: auto;
|
||||
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
||||
scrollbar-width: thin;
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
black 5%,
|
||||
black 94%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
.forecast > div {
|
||||
text-align: center;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.forecast .icon,
|
||||
.forecast .temp {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.forecast .temp {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.forecast-image-icon {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.forecast-image-icon > * {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
--mdc-icon-size: 40px;
|
||||
}
|
||||
|
||||
.forecast-icon {
|
||||
--mdc-icon-size: 40px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private _showValue(item: number | string | undefined): boolean {
|
||||
return typeof item !== "undefined" && item !== null;
|
||||
|
@@ -47,6 +47,8 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
|
||||
@state() private _assistConfiguration?: AssistSatelliteConfiguration;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _previousSteps: STEP[] = [];
|
||||
|
||||
private _nextStep?: STEP;
|
||||
@@ -165,79 +167,86 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
"update"
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-update>`
|
||||
: assistEntityState?.state === UNAVAILABLE
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.satellite_wizard.not_available"
|
||||
)
|
||||
: this._step === STEP.CHECK
|
||||
? html`<ha-voice-assistant-setup-step-check
|
||||
.hass=${this.hass}
|
||||
.assistEntityId=${assistSatelliteEntityId}
|
||||
></ha-voice-assistant-setup-step-check>`
|
||||
: this._step === STEP.WAKEWORD
|
||||
? html`<ha-voice-assistant-setup-step-wake-word
|
||||
: this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: assistEntityState?.state === UNAVAILABLE
|
||||
? html`<ha-alert alert-type="error"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.satellite_wizard.not_available"
|
||||
)}</ha-alert
|
||||
>`
|
||||
: this._step === STEP.CHECK
|
||||
? html`<ha-voice-assistant-setup-step-check
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this._assistConfiguration}
|
||||
.assistEntityId=${assistSatelliteEntityId}
|
||||
.deviceEntities=${this._deviceEntities(
|
||||
this._params.deviceId,
|
||||
this.hass.entities
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-wake-word>`
|
||||
: this._step === STEP.CHANGE_WAKEWORD
|
||||
? html`
|
||||
<ha-voice-assistant-setup-step-change-wake-word
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this._assistConfiguration}
|
||||
.assistEntityId=${assistSatelliteEntityId}
|
||||
></ha-voice-assistant-setup-step-change-wake-word>
|
||||
`
|
||||
: this._step === STEP.AREA
|
||||
></ha-voice-assistant-setup-step-check>`
|
||||
: this._step === STEP.WAKEWORD
|
||||
? html`<ha-voice-assistant-setup-step-wake-word
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this._assistConfiguration}
|
||||
.assistEntityId=${assistSatelliteEntityId}
|
||||
.deviceEntities=${this._deviceEntities(
|
||||
this._params.deviceId,
|
||||
this.hass.entities
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-wake-word>`
|
||||
: this._step === STEP.CHANGE_WAKEWORD
|
||||
? html`
|
||||
<ha-voice-assistant-setup-step-area
|
||||
.hass=${this.hass}
|
||||
.deviceId=${this._params.deviceId}
|
||||
></ha-voice-assistant-setup-step-area>
|
||||
`
|
||||
: this._step === STEP.PIPELINE
|
||||
? html`<ha-voice-assistant-setup-step-pipeline
|
||||
<ha-voice-assistant-setup-step-change-wake-word
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this._assistConfiguration}
|
||||
.assistEntityId=${assistSatelliteEntityId}
|
||||
></ha-voice-assistant-setup-step-pipeline>`
|
||||
: this._step === STEP.CLOUD
|
||||
? html`<ha-voice-assistant-setup-step-cloud
|
||||
></ha-voice-assistant-setup-step-change-wake-word>
|
||||
`
|
||||
: this._step === STEP.AREA
|
||||
? html`
|
||||
<ha-voice-assistant-setup-step-area
|
||||
.hass=${this.hass}
|
||||
></ha-voice-assistant-setup-step-cloud>`
|
||||
: this._step === STEP.LOCAL
|
||||
? html`<ha-voice-assistant-setup-step-local
|
||||
.deviceId=${this._params.deviceId}
|
||||
></ha-voice-assistant-setup-step-area>
|
||||
`
|
||||
: this._step === STEP.PIPELINE
|
||||
? html`<ha-voice-assistant-setup-step-pipeline
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this._assistConfiguration}
|
||||
.assistEntityId=${assistSatelliteEntityId}
|
||||
></ha-voice-assistant-setup-step-pipeline>`
|
||||
: this._step === STEP.CLOUD
|
||||
? html`<ha-voice-assistant-setup-step-cloud
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this
|
||||
._assistConfiguration}
|
||||
></ha-voice-assistant-setup-step-local>`
|
||||
: this._step === STEP.SUCCESS
|
||||
? html`<ha-voice-assistant-setup-step-success
|
||||
></ha-voice-assistant-setup-step-cloud>`
|
||||
: this._step === STEP.LOCAL
|
||||
? html`<ha-voice-assistant-setup-step-local
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this
|
||||
._assistConfiguration}
|
||||
.assistEntityId=${assistSatelliteEntityId}
|
||||
></ha-voice-assistant-setup-step-success>`
|
||||
: nothing}
|
||||
></ha-voice-assistant-setup-step-local>`
|
||||
: this._step === STEP.SUCCESS
|
||||
? html`<ha-voice-assistant-setup-step-success
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this
|
||||
._assistConfiguration}
|
||||
.assistEntityId=${assistSatelliteEntityId}
|
||||
></ha-voice-assistant-setup-step-success>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchAssistConfiguration() {
|
||||
this._assistConfiguration = await fetchAssistSatelliteConfiguration(
|
||||
this.hass,
|
||||
this._findDomainEntityId(
|
||||
this._params!.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
)!
|
||||
);
|
||||
return this._assistConfiguration;
|
||||
try {
|
||||
this._assistConfiguration = await fetchAssistSatelliteConfiguration(
|
||||
this.hass,
|
||||
this._findDomainEntityId(
|
||||
this._params!.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
)!
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
private _goToPreviousStep() {
|
||||
@@ -293,6 +302,10 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
.skip-btn {
|
||||
margin-top: 6px;
|
||||
}
|
||||
ha-alert {
|
||||
margin: 24px;
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -85,7 +85,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
<div class="rows">
|
||||
${this.assistConfiguration &&
|
||||
this.assistConfiguration.available_wake_words.length > 1
|
||||
? html` <div class="row">
|
||||
? html`<div class="row">
|
||||
<ha-select
|
||||
.label=${"Wake word"}
|
||||
@closed=${stopPropagation}
|
||||
|
@@ -44,6 +44,15 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
|
||||
protected override willUpdate(changedProperties: PropertyValues) {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (changedProperties.has("assistConfiguration")) {
|
||||
if (
|
||||
this.assistConfiguration &&
|
||||
!this.assistConfiguration.available_wake_words.length
|
||||
) {
|
||||
this._nextStep();
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has("assistEntityId")) {
|
||||
this._detected = false;
|
||||
this._muteSwitchEntity = this.deviceEntities?.find(
|
||||
@@ -135,13 +144,16 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
|
||||
>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="footer centered">
|
||||
<ha-button @click=${this._changeWakeWord}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.satellite_wizard.wake_word.change_wake_word"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>`;
|
||||
${this.assistConfiguration &&
|
||||
this.assistConfiguration.available_wake_words.length > 1
|
||||
? html`<div class="footer centered">
|
||||
<ha-button @click=${this._changeWakeWord}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.satellite_wizard.wake_word.change_wake_word"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>`
|
||||
: nothing}`;
|
||||
}
|
||||
|
||||
private async _listenWakeWord() {
|
||||
|
@@ -20,6 +20,12 @@
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<%= renderTemplate("_style_base.html.template") %>
|
||||
<style>
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
color: var(--primary-text-color, #212121);
|
||||
|
@@ -95,7 +95,10 @@ export class HassRouterPage extends ReactiveElement {
|
||||
const defaultPage = routerOptions.defaultPage;
|
||||
|
||||
if (route && route.path === "" && defaultPage !== undefined) {
|
||||
navigate(`${route.prefix}/${defaultPage}`, { replace: true });
|
||||
const queryParams = window.location.search;
|
||||
navigate(`${route.prefix}/${defaultPage}${queryParams}`, {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
|
||||
let newPage = route
|
||||
|
@@ -5,7 +5,7 @@ import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiClose,
|
||||
mdiCog,
|
||||
mdiTableCog,
|
||||
mdiFilterVariant,
|
||||
mdiFilterVariantRemove,
|
||||
mdiFormatListChecks,
|
||||
@@ -309,7 +309,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
@click=${this._openSettings}
|
||||
.title=${localize("ui.components.subpage-data-table.settings")}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
|
||||
<ha-svg-icon slot="icon" .path=${mdiTableCog}></ha-svg-icon>
|
||||
</ha-assist-chip>`;
|
||||
|
||||
return html`
|
||||
@@ -355,7 +355,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
></ha-assist-chip>
|
||||
<ha-md-menu-item
|
||||
.value=${undefined}
|
||||
@click=${this._selectAll}
|
||||
.clickAction=${this._selectAll}
|
||||
>
|
||||
<div slot="headline">
|
||||
${localize("ui.components.subpage-data-table.select_all")}
|
||||
@@ -363,7 +363,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.value=${undefined}
|
||||
@click=${this._selectNone}
|
||||
.clickAction=${this._selectNone}
|
||||
>
|
||||
<div slot="headline">
|
||||
${localize(
|
||||
@@ -374,7 +374,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item
|
||||
.value=${undefined}
|
||||
@click=${this._disableSelectMode}
|
||||
.clickAction=${this._disableSelectMode}
|
||||
>
|
||||
<div slot="headline">
|
||||
${localize(
|
||||
@@ -500,7 +500,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
.value=${id}
|
||||
@click=${this._handleGroupBy}
|
||||
.clickAction=${this._handleGroupBy}
|
||||
.selected=${id === this._groupColumn}
|
||||
class=${classMap({ selected: id === this._groupColumn })}
|
||||
>
|
||||
@@ -511,7 +511,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
)}
|
||||
<ha-md-menu-item
|
||||
.value=${undefined}
|
||||
@click=${this._handleGroupBy}
|
||||
.clickAction=${this._handleGroupBy}
|
||||
.selected=${this._groupColumn === undefined}
|
||||
class=${classMap({ selected: this._groupColumn === undefined })}
|
||||
>
|
||||
@@ -519,7 +519,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item
|
||||
@click=${this._collapseAllGroups}
|
||||
.clickAction=${this._collapseAllGroups}
|
||||
.disabled=${this._groupColumn === undefined}
|
||||
>
|
||||
<ha-svg-icon
|
||||
@@ -529,7 +529,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
${localize("ui.components.subpage-data-table.collapse_all_groups")}
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
@click=${this._expandAllGroups}
|
||||
.clickAction=${this._expandAllGroups}
|
||||
.disabled=${this._groupColumn === undefined}
|
||||
>
|
||||
<ha-svg-icon
|
||||
@@ -546,6 +546,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
<ha-md-menu-item
|
||||
.value=${id}
|
||||
@click=${this._handleSortBy}
|
||||
@keydown=${this._handleSortBy}
|
||||
keep-open
|
||||
.selected=${id === this._sortColumn}
|
||||
class=${classMap({ selected: id === this._sortColumn })}
|
||||
@@ -623,6 +624,8 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _handleSortBy(ev) {
|
||||
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") return;
|
||||
|
||||
const columnId = ev.currentTarget.value;
|
||||
if (!this._sortDirection || this._sortColumn !== columnId) {
|
||||
this._sortDirection = "asc";
|
||||
@@ -639,9 +642,9 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleGroupBy(ev) {
|
||||
this._setGroupColumn(ev.currentTarget.value);
|
||||
}
|
||||
private _handleGroupBy = (item) => {
|
||||
this._setGroupColumn(item.value);
|
||||
};
|
||||
|
||||
private _setGroupColumn(columnId: string) {
|
||||
this._groupColumn = columnId;
|
||||
@@ -665,30 +668,30 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _collapseAllGroups() {
|
||||
private _collapseAllGroups = () => {
|
||||
this._dataTable.collapseAllGroups();
|
||||
}
|
||||
};
|
||||
|
||||
private _expandAllGroups() {
|
||||
private _expandAllGroups = () => {
|
||||
this._dataTable.expandAllGroups();
|
||||
}
|
||||
};
|
||||
|
||||
private _enableSelectMode() {
|
||||
this._selectMode = true;
|
||||
}
|
||||
|
||||
private _disableSelectMode() {
|
||||
private _disableSelectMode = () => {
|
||||
this._selectMode = false;
|
||||
this._dataTable.clearSelection();
|
||||
}
|
||||
};
|
||||
|
||||
private _selectAll() {
|
||||
private _selectAll = () => {
|
||||
this._dataTable.selectAll();
|
||||
}
|
||||
};
|
||||
|
||||
private _selectNone() {
|
||||
private _selectNone = () => {
|
||||
this._dataTable.clearSelection();
|
||||
}
|
||||
};
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
if (this.filter === ev.detail.value) {
|
||||
|
@@ -17,7 +17,7 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
|
||||
@property({ attribute: false }) public localize: LocalizeFunc = empty;
|
||||
|
||||
// Use browser language setup before login.
|
||||
@property() public language?: string = getLocalLanguage();
|
||||
@property() public language: string = getLocalLanguage();
|
||||
|
||||
@property() public translationFragment?: string;
|
||||
|
||||
|
@@ -41,6 +41,7 @@ import "./onboarding-analytics";
|
||||
import "./onboarding-create-user";
|
||||
import "./onboarding-loading";
|
||||
import "./onboarding-welcome";
|
||||
import "./onboarding-restore-backup";
|
||||
import "./onboarding-welcome-links";
|
||||
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
||||
import { navigate } from "../common/navigate";
|
||||
@@ -157,8 +158,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
private _renderStep() {
|
||||
if (this._restoring) {
|
||||
return html`<onboarding-restore-backup
|
||||
.hass=${this.hass}
|
||||
.localize=${this.localize}
|
||||
.supervisor=${this._supervisor ?? false}
|
||||
.language=${this.language}
|
||||
>
|
||||
</onboarding-restore-backup>`;
|
||||
}
|
||||
@@ -166,8 +168,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
if (this._init) {
|
||||
return html`<onboarding-welcome
|
||||
.localize=${this.localize}
|
||||
.language=${this.language}
|
||||
.supervisor=${this._supervisor}
|
||||
></onboarding-welcome>`;
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
}
|
||||
}
|
||||
if (changedProps.has("language")) {
|
||||
document.querySelector("html")!.setAttribute("lang", this.language!);
|
||||
document.querySelector("html")!.setAttribute("lang", this.language);
|
||||
}
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
@@ -272,10 +272,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
"Home Assistant OS",
|
||||
"Home Assistant Supervised",
|
||||
].includes(response.installation_type);
|
||||
if (this._supervisor) {
|
||||
// Only load if we have supervisor
|
||||
import("./onboarding-restore-backup");
|
||||
}
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
@@ -454,7 +450,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
subscribeOne(conn, subscribeUser),
|
||||
]);
|
||||
this.initializeHass(auth, conn);
|
||||
if (this.language && this.language !== this.hass!.language) {
|
||||
if (this.language !== this.hass!.language) {
|
||||
this._updateHass({
|
||||
locale: { ...this.hass!.locale, language: this.language },
|
||||
language: this.language,
|
||||
|
@@ -1,136 +1,336 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
|
||||
import "../../hassio/src/components/hassio-upload-backup";
|
||||
import "./restore-backup/onboarding-restore-backup-upload";
|
||||
import "./restore-backup/onboarding-restore-backup-details";
|
||||
import "./restore-backup/onboarding-restore-backup-restore";
|
||||
import "./restore-backup/onboarding-restore-backup-status";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-ansi-to-html";
|
||||
import "../components/ha-card";
|
||||
import "../components/ha-icon-button-arrow-prev";
|
||||
import "../components/ha-circular-progress";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-button";
|
||||
import { fetchInstallationType } from "../data/onboarding";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./onboarding-loading";
|
||||
import { onBoardingStyles } from "./styles";
|
||||
import { removeSearchParam } from "../common/url/search-params";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { onBoardingStyles } from "./styles";
|
||||
import {
|
||||
fetchBackupOnboardingInfo,
|
||||
type BackupOnboardingConfig,
|
||||
type BackupOnboardingInfo,
|
||||
} from "../data/backup_onboarding";
|
||||
import type { BackupContentExtended, BackupData } from "../data/backup";
|
||||
import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { storage } from "../common/decorators/storage";
|
||||
|
||||
const STATUS_INTERVAL_IN_MS = 5000;
|
||||
|
||||
@customElement("onboarding-restore-backup")
|
||||
class OnboardingRestoreBackup extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
@property() public language!: string;
|
||||
|
||||
@state() private _restoring = false;
|
||||
@property({ type: Boolean }) public supervisor = false;
|
||||
|
||||
@state() private _backupSlug?: string;
|
||||
@state() private _view:
|
||||
| "loading"
|
||||
| "upload"
|
||||
| "select_data"
|
||||
| "confirm_restore"
|
||||
| "status" = "loading";
|
||||
|
||||
@state() private _backup?: BackupContentExtended;
|
||||
|
||||
@state() private _backupInfo?: BackupOnboardingInfo;
|
||||
|
||||
@state() private _selectedData?: BackupData;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _failed?: boolean;
|
||||
|
||||
@storage({
|
||||
key: "onboarding-restore-backup-backup-id",
|
||||
})
|
||||
private _backupId?: string;
|
||||
|
||||
@storage({
|
||||
key: "onboarding-restore-running",
|
||||
})
|
||||
private _restoreRunning?: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this._restoring
|
||||
? html`<h1>
|
||||
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
|
||||
</h1>
|
||||
<ha-alert alert-type="info">
|
||||
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
|
||||
</ha-alert>
|
||||
<onboarding-loading></onboarding-loading>`
|
||||
: html` <h1>
|
||||
${this.localize("ui.panel.page-onboarding.restore.header")}
|
||||
</h1>
|
||||
<hassio-upload-backup
|
||||
@backup-uploaded=${this._backupUploaded}
|
||||
@backup-cleared=${this._backupCleared}
|
||||
.hass=${this.hass}
|
||||
.localize=${this.localize}
|
||||
></hassio-upload-backup>`}
|
||||
<div class="footer">
|
||||
<ha-button @click=${this._back} .disabled=${this._restoring}>
|
||||
${this.localize("ui.panel.page-onboarding.back")}
|
||||
</ha-button>
|
||||
${this._backupSlug
|
||||
? html`<ha-button
|
||||
@click=${this._showBackupDialog}
|
||||
.disabled=${this._restoring}
|
||||
${
|
||||
this._view !== "status" || this._failed
|
||||
? html`<ha-icon-button-arrow-prev
|
||||
.label=${this.localize("ui.panel.page-onboarding.restore.back")}
|
||||
@click=${this._back}
|
||||
></ha-icon-button-arrow-prev>`
|
||||
: nothing
|
||||
}
|
||||
</ha-icon-button>
|
||||
<h1>${this.localize("ui.panel.page-onboarding.restore.header")}</h1>
|
||||
${
|
||||
this._error || (this._failed && this._view !== "status")
|
||||
? html`<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this._failed && this._view !== "status"
|
||||
? this.localize("ui.panel.page-onboarding.restore.failed")
|
||||
: ""}
|
||||
>
|
||||
${this.localize("ui.panel.page-onboarding.restore.restore")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._failed && this._view !== "status"
|
||||
? this.localize(
|
||||
`ui.panel.page-onboarding.restore.${this._backupInfo?.last_non_idle_event?.reason === "password_incorrect" ? "failed_wrong_password_description" : "failed_description"}`
|
||||
)
|
||||
: this._error}
|
||||
</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._view === "loading"
|
||||
? html`<div class="loading">
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||
</div>`
|
||||
: this._view === "upload"
|
||||
? html`
|
||||
<onboarding-restore-backup-upload
|
||||
.supervisor=${this.supervisor}
|
||||
.localize=${this.localize}
|
||||
@backup-uploaded=${this._backupUploaded}
|
||||
></onboarding-restore-backup-upload>
|
||||
`
|
||||
: this._view === "select_data"
|
||||
? html`<onboarding-restore-backup-details
|
||||
.localize=${this.localize}
|
||||
.backup=${this._backup!}
|
||||
@backup-restore=${this._restore}
|
||||
></onboarding-restore-backup-details>`
|
||||
: this._view === "confirm_restore"
|
||||
? html`<onboarding-restore-backup-restore
|
||||
.localize=${this.localize}
|
||||
.backup=${this._backup!}
|
||||
.supervisor=${this.supervisor}
|
||||
.selectedData=${this._selectedData!}
|
||||
@restore-started=${this._restoreStarted}
|
||||
></onboarding-restore-backup-restore>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._view === "status" && this._backupInfo
|
||||
? html`<onboarding-restore-backup-status
|
||||
.localize=${this.localize}
|
||||
.backupInfo=${this._backupInfo}
|
||||
@show-backup-upload=${this._reupload}
|
||||
></onboarding-restore-backup-status>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
["select_data", "confirm_restore"].includes(this._view) && this._backup
|
||||
? html`<div class="backup-summary-wrapper">
|
||||
<ha-backup-details-summary
|
||||
translation-key-panel="page-onboarding.restore"
|
||||
show-upload-another
|
||||
.backup=${this._backup}
|
||||
.localize=${this.localize}
|
||||
@show-backup-upload=${this._reupload}
|
||||
.isHassio=${this.supervisor}
|
||||
></ha-backup-details-summary>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _back(): void {
|
||||
navigate(`${location.pathname}?${removeSearchParam("page")}`);
|
||||
}
|
||||
|
||||
private _backupUploaded(ev) {
|
||||
const backup = ev.detail.backup;
|
||||
this._backupSlug = backup.slug;
|
||||
this._showBackupDialog();
|
||||
}
|
||||
|
||||
private _backupCleared() {
|
||||
this._backupSlug = undefined;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this._loadBackupInfo();
|
||||
}
|
||||
|
||||
private async _checkRestoreStatus(): Promise<void> {
|
||||
if (this._restoring) {
|
||||
try {
|
||||
await fetchInstallationType();
|
||||
} catch (err: any) {
|
||||
private async _loadBackupInfo() {
|
||||
let onboardingInfo: BackupOnboardingConfig;
|
||||
try {
|
||||
onboardingInfo = await fetchBackupOnboardingInfo();
|
||||
} catch (err: any) {
|
||||
if (this._restoreRunning) {
|
||||
if (
|
||||
(err as Error).message === "unauthorized" ||
|
||||
(err as Error).message === "not_found"
|
||||
err.error === "Request error" ||
|
||||
// core can restart but haven't loaded the backup integration yet
|
||||
(err.status_code === 500 && err.body?.error === "backup_disabled")
|
||||
) {
|
||||
// core is down because of restore, keep trying
|
||||
this._scheduleLoadBackupInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
// core seems to be back up restored
|
||||
if (err.status_code === 404) {
|
||||
this._restoreRunning = undefined;
|
||||
this._backupId = undefined;
|
||||
window.location.replace("/");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._error = err?.message || "Cannot get backup info";
|
||||
|
||||
// if we are in an unknown state, show upload
|
||||
if (this._view === "loading") {
|
||||
this._view = "upload";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
last_non_idle_event: lastNonIdleEvent,
|
||||
state: currentState,
|
||||
backups,
|
||||
} = onboardingInfo;
|
||||
|
||||
this._backupInfo = {
|
||||
state: currentState,
|
||||
last_non_idle_event: lastNonIdleEvent,
|
||||
};
|
||||
|
||||
if (this._backupId) {
|
||||
this._backup = backups.find(
|
||||
({ backup_id }) => backup_id === this._backupId
|
||||
);
|
||||
}
|
||||
|
||||
const failedRestore =
|
||||
lastNonIdleEvent?.manager_state === "restore_backup" &&
|
||||
lastNonIdleEvent?.state === "failed";
|
||||
|
||||
if (failedRestore) {
|
||||
this._failed = true;
|
||||
}
|
||||
|
||||
if (this._restoreRunning) {
|
||||
this._view = "status";
|
||||
if (failedRestore || currentState !== "restore_backup") {
|
||||
this._failed = true;
|
||||
this._restoreRunning = undefined;
|
||||
} else {
|
||||
this._scheduleLoadBackupInfo();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this._backup &&
|
||||
// after backup was uploaded
|
||||
(lastNonIdleEvent?.manager_state === "receive_backup" ||
|
||||
// when restore was confirmed but failed to start (for example, encryption key was wrong)
|
||||
failedRestore)
|
||||
) {
|
||||
if (!this.supervisor && this._backup.homeassistant_included) {
|
||||
this._selectedData = {
|
||||
homeassistant_included: true,
|
||||
folders: [],
|
||||
addons: [],
|
||||
homeassistant_version: this._backup.homeassistant_version,
|
||||
database_included: this._backup.database_included,
|
||||
};
|
||||
// skip select data when supervisor is not available and backup includes HA
|
||||
this._view = "confirm_restore";
|
||||
} else {
|
||||
this._view = "select_data";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// show upload as default
|
||||
this._view = "upload";
|
||||
}
|
||||
|
||||
private _scheduleLoadBackupInfo() {
|
||||
setTimeout(() => this._loadBackupInfo(), STATUS_INTERVAL_IN_MS);
|
||||
}
|
||||
|
||||
private async _backupUploaded(ev: CustomEvent) {
|
||||
this._backupId = ev.detail.backupId;
|
||||
await this._loadBackupInfo();
|
||||
}
|
||||
|
||||
private async _restoreStarted() {
|
||||
if (this._backupInfo) {
|
||||
this._backupInfo.state = "restore_backup";
|
||||
}
|
||||
this._view = "status";
|
||||
this._restoreRunning = true;
|
||||
await this._loadBackupInfo();
|
||||
}
|
||||
|
||||
private async _back() {
|
||||
if (this._view === "upload" || (this._view === "status" && this._failed)) {
|
||||
navigate(`${location.pathname}?${removeSearchParam("page")}`);
|
||||
} else {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.localize(
|
||||
"ui.panel.page-onboarding.restore.cancel_restore.title"
|
||||
),
|
||||
text: this.localize(
|
||||
"ui.panel.page-onboarding.restore.cancel_restore.text"
|
||||
),
|
||||
confirmText: this.localize(
|
||||
"ui.panel.page-onboarding.restore.cancel_restore.yes"
|
||||
),
|
||||
dismissText: this.localize(
|
||||
"ui.panel.page-onboarding.restore.cancel_restore.no"
|
||||
),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
navigate(`${location.pathname}?${removeSearchParam("page")}`);
|
||||
}
|
||||
}
|
||||
|
||||
private _scheduleCheckRestoreStatus(): void {
|
||||
setTimeout(() => this._checkRestoreStatus(), 1000);
|
||||
private _restore(ev: CustomEvent) {
|
||||
if (!this._backup || !ev.detail.selectedData) {
|
||||
return;
|
||||
}
|
||||
this._selectedData = ev.detail.selectedData;
|
||||
|
||||
this._view = "confirm_restore";
|
||||
}
|
||||
|
||||
private _showBackupDialog(): void {
|
||||
showHassioBackupDialog(this, {
|
||||
slug: this._backupSlug!,
|
||||
onboarding: true,
|
||||
localize: this.localize,
|
||||
onRestoring: () => {
|
||||
this._restoring = true;
|
||||
this._scheduleCheckRestoreStatus();
|
||||
},
|
||||
});
|
||||
private _reupload() {
|
||||
this._backup = undefined;
|
||||
this._backupId = undefined;
|
||||
this._view = "upload";
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
onBoardingStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
hassio-upload-backup {
|
||||
width: 100%;
|
||||
}
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
static styles = [
|
||||
onBoardingStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
ha-icon-button-arrow-prev {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
}
|
||||
ha-card {
|
||||
width: 100%;
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
}
|
||||
.backup-summary-wrapper {
|
||||
margin-top: 24px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user