mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-08 12:46:29 +00:00
Compare commits
387 Commits
20200715.1
...
script-yam
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bda951e6d1 | ||
![]() |
92ed14c0e4 | ||
![]() |
5b94a4de9a | ||
![]() |
709112c498 | ||
![]() |
e465ec8835 | ||
![]() |
f6eb31bf9d | ||
![]() |
426f939982 | ||
![]() |
fab6cebf0d | ||
![]() |
ff081dd0f1 | ||
![]() |
868399ed6f | ||
![]() |
1bc9b95289 | ||
![]() |
9af805ab5e | ||
![]() |
6b88081360 | ||
![]() |
50d37ce4f6 | ||
![]() |
af0246cd27 | ||
![]() |
857e4e49d8 | ||
![]() |
c1afed7f98 | ||
![]() |
5480e54185 | ||
![]() |
99d0a0a6fd | ||
![]() |
8a998369d6 | ||
![]() |
8b490c5047 | ||
![]() |
7e70ba6ab2 | ||
![]() |
90e09fc384 | ||
![]() |
266f2e763d | ||
![]() |
c979cfb912 | ||
![]() |
8ee29b1e43 | ||
![]() |
26fbc07cac | ||
![]() |
f01fe65be4 | ||
![]() |
3fdd6a80f9 | ||
![]() |
da1de8db1d | ||
![]() |
dd1bf7b49d | ||
![]() |
f18913b5a0 | ||
![]() |
a2cd227f1a | ||
![]() |
78e64e1f60 | ||
![]() |
23a9b79320 | ||
![]() |
76394ce341 | ||
![]() |
1935df1faa | ||
![]() |
5af4ce28ab | ||
![]() |
ce8ee569c4 | ||
![]() |
b0508f430e | ||
![]() |
2139a80a7a | ||
![]() |
aa4bc2ce03 | ||
![]() |
fa65f84e09 | ||
![]() |
c06357a351 | ||
![]() |
092a02a624 | ||
![]() |
b9699f745f | ||
![]() |
7fa9f10c30 | ||
![]() |
7bf0655dae | ||
![]() |
96c5fdcbeb | ||
![]() |
c2e6d40382 | ||
![]() |
810d2a1ceb | ||
![]() |
af74f21af9 | ||
![]() |
cdf7558a8e | ||
![]() |
41b86e6c10 | ||
![]() |
3039c678a5 | ||
![]() |
498882d014 | ||
![]() |
6c2b8c2abb | ||
![]() |
e955cc4378 | ||
![]() |
eb96dd4803 | ||
![]() |
e0bdef98a6 | ||
![]() |
1130007d14 | ||
![]() |
d99d092784 | ||
![]() |
e3b18a33ca | ||
![]() |
1890aab1e6 | ||
![]() |
42bf350034 | ||
![]() |
5ff52ea113 | ||
![]() |
432e3ba636 | ||
![]() |
f7ab52fe9a | ||
![]() |
ad8430049d | ||
![]() |
2dffe7ba9e | ||
![]() |
5b8f97e0f6 | ||
![]() |
b3a763a48d | ||
![]() |
07569f10b5 | ||
![]() |
7c5a78a1cf | ||
![]() |
0e021e7d7d | ||
![]() |
b30ee884a7 | ||
![]() |
869b7c85ca | ||
![]() |
4d0d1ed2a1 | ||
![]() |
291983e4c3 | ||
![]() |
909cff2158 | ||
![]() |
4e676b1dba | ||
![]() |
9149bb9333 | ||
![]() |
4631994f20 | ||
![]() |
82e9178320 | ||
![]() |
67b4688168 | ||
![]() |
6e0e169b6e | ||
![]() |
100ba8edfa | ||
![]() |
d7448ecb95 | ||
![]() |
8b1801f378 | ||
![]() |
01a4d57566 | ||
![]() |
7edc9064d9 | ||
![]() |
30c47a65f4 | ||
![]() |
0889f42a00 | ||
![]() |
f15fbe53cf | ||
![]() |
046f7b5153 | ||
![]() |
5339fe6e06 | ||
![]() |
de7ffb10cb | ||
![]() |
80224e6974 | ||
![]() |
0c7c536f73 | ||
![]() |
e5c386c39a | ||
![]() |
bb2462483e | ||
![]() |
d5bc498373 | ||
![]() |
979b7ae651 | ||
![]() |
c73330a466 | ||
![]() |
efe8eca4e3 | ||
![]() |
a37aad18b7 | ||
![]() |
cfa0c45213 | ||
![]() |
509481ef06 | ||
![]() |
9aa8175e23 | ||
![]() |
76f59d99a2 | ||
![]() |
bd66bd6cf0 | ||
![]() |
d69333dea4 | ||
![]() |
3fd7899b93 | ||
![]() |
8f8a2cea56 | ||
![]() |
879011c8e9 | ||
![]() |
d5794c3e2e | ||
![]() |
fcc22ba560 | ||
![]() |
2adeb88fe6 | ||
![]() |
e63a78bcdb | ||
![]() |
b065f002a4 | ||
![]() |
349a5f52b1 | ||
![]() |
aa5e20df05 | ||
![]() |
793b9f238c | ||
![]() |
9c4fdaa4f3 | ||
![]() |
d1a9cb488a | ||
![]() |
faee2c3e1b | ||
![]() |
b7845c318e | ||
![]() |
426a0727c3 | ||
![]() |
584e509a9c | ||
![]() |
f3639c2663 | ||
![]() |
1431e75f8b | ||
![]() |
be8812e0af | ||
![]() |
fd6436d490 | ||
![]() |
fd1342f9d1 | ||
![]() |
5fa0012195 | ||
![]() |
9dbb67ef01 | ||
![]() |
d16e2f37d4 | ||
![]() |
d9e8b53ffe | ||
![]() |
1997e63b7c | ||
![]() |
6f673359ff | ||
![]() |
45dfbff10a | ||
![]() |
348ee96274 | ||
![]() |
8edee32e77 | ||
![]() |
6d8d263ca6 | ||
![]() |
35923709e2 | ||
![]() |
fdd4d53448 | ||
![]() |
06419f662e | ||
![]() |
57763ef032 | ||
![]() |
8e506f7749 | ||
![]() |
c7f8fe1468 | ||
![]() |
4156a4e36d | ||
![]() |
0c212d39eb | ||
![]() |
3bd2e8dbf5 | ||
![]() |
5292119e6e | ||
![]() |
994a397231 | ||
![]() |
353b71f803 | ||
![]() |
eb12afe8cc | ||
![]() |
4a176f1b43 | ||
![]() |
8e228baa82 | ||
![]() |
154b53b0d8 | ||
![]() |
a3f680d80c | ||
![]() |
0d75fe6b81 | ||
![]() |
4070380ded | ||
![]() |
41195dcef0 | ||
![]() |
78a1e45be2 | ||
![]() |
d8e88bc58d | ||
![]() |
448e9b71b8 | ||
![]() |
2e178164cc | ||
![]() |
9f2e3f05a1 | ||
![]() |
405bd29ebd | ||
![]() |
b39b54e0ac | ||
![]() |
119c5c9071 | ||
![]() |
7a4c9b128c | ||
![]() |
dc5b92030f | ||
![]() |
db0a010d7c | ||
![]() |
a117a19bdf | ||
![]() |
5f46fdb406 | ||
![]() |
f0201de4cc | ||
![]() |
6cd51a318b | ||
![]() |
c1a4b27bc7 | ||
![]() |
5113222050 | ||
![]() |
90f12eea5e | ||
![]() |
2403743701 | ||
![]() |
3e6a759309 | ||
![]() |
35a430e9f4 | ||
![]() |
b644f7d23d | ||
![]() |
7702a05464 | ||
![]() |
493af5fe82 | ||
![]() |
ac66a59cec | ||
![]() |
e10c8faa47 | ||
![]() |
9b7d17433c | ||
![]() |
a40eb1ff43 | ||
![]() |
04df6c3e9e | ||
![]() |
1b970e5a66 | ||
![]() |
75406c2d01 | ||
![]() |
64d3511fbc | ||
![]() |
c610f54977 | ||
![]() |
358c5205d2 | ||
![]() |
5503cd0589 | ||
![]() |
dae42b1bd9 | ||
![]() |
06a25284e8 | ||
![]() |
5989560f15 | ||
![]() |
63c995e5da | ||
![]() |
dc5607f554 | ||
![]() |
d49302c032 | ||
![]() |
63fef9bd4b | ||
![]() |
6599351d45 | ||
![]() |
47e9531972 | ||
![]() |
3ba31483f4 | ||
![]() |
f4ca94f2e1 | ||
![]() |
67f9be2b77 | ||
![]() |
e2fd155e1b | ||
![]() |
931068dede | ||
![]() |
bc4c9cc40d | ||
![]() |
294665fbe8 | ||
![]() |
e8f6a79c8f | ||
![]() |
5fd8b5c5b9 | ||
![]() |
226b2a73af | ||
![]() |
42d421a6fc | ||
![]() |
a90203f256 | ||
![]() |
c3ef79caa9 | ||
![]() |
1439afcd9c | ||
![]() |
d263b19910 | ||
![]() |
1e477226ea | ||
![]() |
026fc1d2e3 | ||
![]() |
2d4bd9857a | ||
![]() |
8f48f5b45c | ||
![]() |
22210b7400 | ||
![]() |
7d05855ee0 | ||
![]() |
4561957e56 | ||
![]() |
3367fadc3a | ||
![]() |
d7e409b042 | ||
![]() |
a0b28e8ad1 | ||
![]() |
f928a8e58e | ||
![]() |
0bc4b3d0fa | ||
![]() |
e352768388 | ||
![]() |
6835b73e49 | ||
![]() |
f1503f871b | ||
![]() |
c4d8aba5c8 | ||
![]() |
39f24c41ad | ||
![]() |
21644ec889 | ||
![]() |
613470b44d | ||
![]() |
6c918e346b | ||
![]() |
bce8539572 | ||
![]() |
aab86e00ec | ||
![]() |
2a58726caf | ||
![]() |
4163b35b32 | ||
![]() |
9c6dac8180 | ||
![]() |
80fc37724b | ||
![]() |
77b25f5132 | ||
![]() |
684f098450 | ||
![]() |
d09f74d30f | ||
![]() |
3d973b112e | ||
![]() |
96986164a4 | ||
![]() |
78152c20a9 | ||
![]() |
2bb64e9e2f | ||
![]() |
746844dfc8 | ||
![]() |
41b613a2d7 | ||
![]() |
3b3aeea224 | ||
![]() |
71c592a0ce | ||
![]() |
15193fcf5f | ||
![]() |
a31f53395f | ||
![]() |
283b134d84 | ||
![]() |
271eb614cd | ||
![]() |
16167bef07 | ||
![]() |
1eac9fa1cd | ||
![]() |
7f819f0020 | ||
![]() |
dec1f99a5f | ||
![]() |
c705e74fc8 | ||
![]() |
01df10f93e | ||
![]() |
9877f08cf4 | ||
![]() |
3dc4b1d775 | ||
![]() |
02791c51ae | ||
![]() |
49683326e6 | ||
![]() |
947773a82e | ||
![]() |
2a229df624 | ||
![]() |
e605ad5e46 | ||
![]() |
0d4f43472b | ||
![]() |
b30e467685 | ||
![]() |
a56c0b52d5 | ||
![]() |
c17ebfd279 | ||
![]() |
5400b1da96 | ||
![]() |
69f4a618b2 | ||
![]() |
16b8b6698c | ||
![]() |
b29a700d40 | ||
![]() |
bbb1468439 | ||
![]() |
72f9d6a8d3 | ||
![]() |
3ec8da1f17 | ||
![]() |
dbea3848df | ||
![]() |
33871435e1 | ||
![]() |
f1f22b43dc | ||
![]() |
2fb9a56e0b | ||
![]() |
14e8f66ed7 | ||
![]() |
e6f5072462 | ||
![]() |
a64f50fa72 | ||
![]() |
bb5f6e88d0 | ||
![]() |
6991403203 | ||
![]() |
410bd22f8a | ||
![]() |
b81d823602 | ||
![]() |
bd5115f9aa | ||
![]() |
7bcbed80d7 | ||
![]() |
8fb62ebf5f | ||
![]() |
209dd9923f | ||
![]() |
c75207e391 | ||
![]() |
d957f36927 | ||
![]() |
9ac459b6d9 | ||
![]() |
e08b2817ba | ||
![]() |
4ca13c409b | ||
![]() |
0d515e2303 | ||
![]() |
a2153bc6aa | ||
![]() |
ca171afe6f | ||
![]() |
bf4e97bd48 | ||
![]() |
8c59a12a03 | ||
![]() |
89569355be | ||
![]() |
3a41b3bdcf | ||
![]() |
12bd7037b3 | ||
![]() |
ca4f573be0 | ||
![]() |
07fceeab5a | ||
![]() |
3aa376e912 | ||
![]() |
92d30a8896 | ||
![]() |
83876fb9da | ||
![]() |
29bdf7877c | ||
![]() |
29199e2782 | ||
![]() |
68e1378615 | ||
![]() |
cf7efb5bfc | ||
![]() |
8634ee536d | ||
![]() |
632d3cda24 | ||
![]() |
29b6a907d4 | ||
![]() |
7474d09e5d | ||
![]() |
fc7bcd7e00 | ||
![]() |
f6fb2e4b1d | ||
![]() |
8c8673a272 | ||
![]() |
4404a1173b | ||
![]() |
e08c10315e | ||
![]() |
16473c9177 | ||
![]() |
235fd5603f | ||
![]() |
d07d5832f5 | ||
![]() |
ef8be5d559 | ||
![]() |
ccafdc6e1f | ||
![]() |
11827aa4c0 | ||
![]() |
6b0589d343 | ||
![]() |
cec1eed99e | ||
![]() |
d7e1e9e284 | ||
![]() |
cae46453a7 | ||
![]() |
a6e948c808 | ||
![]() |
7638020bfc | ||
![]() |
10a62ca17c | ||
![]() |
0afc7c184f | ||
![]() |
168e26aeb4 | ||
![]() |
e6b9389b33 | ||
![]() |
377c37425e | ||
![]() |
4af26602bb | ||
![]() |
c6624e5cb6 | ||
![]() |
f7ae5b91bf | ||
![]() |
07e68496c0 | ||
![]() |
d5a947e2cc | ||
![]() |
3f920767f1 | ||
![]() |
3e14d27a1e | ||
![]() |
cfa4c14108 | ||
![]() |
209056dbe1 | ||
![]() |
10356a7496 | ||
![]() |
d4ae74de44 | ||
![]() |
88d5e7dd5e | ||
![]() |
06c7b0b82e | ||
![]() |
689febda60 | ||
![]() |
80bc6fda8b | ||
![]() |
346eb78c4e | ||
![]() |
2df02f1b09 | ||
![]() |
92915eddc2 | ||
![]() |
cddbf460f8 | ||
![]() |
3c63c23e5a | ||
![]() |
ba67b1291f | ||
![]() |
7bced28327 | ||
![]() |
db2b60700c | ||
![]() |
9034822c44 | ||
![]() |
e8254f9aae | ||
![]() |
a14179b81a | ||
![]() |
427c5db7f4 | ||
![]() |
fcb5865468 | ||
![]() |
41370be2b8 | ||
![]() |
d7d8dd8986 | ||
![]() |
a0f596e419 | ||
![]() |
0a8894feb7 | ||
![]() |
1db9eea0f8 | ||
![]() |
489783c398 | ||
![]() |
be62f327ee | ||
![]() |
32359adb6d |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"airbnb-typescript/base",
|
"airbnb-typescript/base",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:wc/recommended",
|
"plugin:wc/recommended",
|
||||||
"plugin:lit/recommended",
|
"plugin:lit/recommended",
|
||||||
"prettier",
|
"prettier",
|
||||||
@@ -45,16 +45,16 @@
|
|||||||
"func-names": 0,
|
"func-names": 0,
|
||||||
"prefer-arrow-callback": 0,
|
"prefer-arrow-callback": 0,
|
||||||
"no-underscore-dangle": 0,
|
"no-underscore-dangle": 0,
|
||||||
"no-var": 0,
|
|
||||||
"strict": 0,
|
"strict": 0,
|
||||||
"prefer-spread": 0,
|
"prefer-spread": 0,
|
||||||
"no-plusplus": 0,
|
"no-plusplus": 0,
|
||||||
"no-bitwise": 0,
|
"no-bitwise": 2,
|
||||||
"comma-dangle": 0,
|
"comma-dangle": 0,
|
||||||
"vars-on-top": 0,
|
"vars-on-top": 0,
|
||||||
"no-continue": 0,
|
"no-continue": 0,
|
||||||
"no-param-reassign": 0,
|
"no-param-reassign": 0,
|
||||||
"no-multi-assign": 0,
|
"no-multi-assign": 0,
|
||||||
|
"no-console": 2,
|
||||||
"radix": 0,
|
"radix": 0,
|
||||||
"no-alert": 0,
|
"no-alert": 0,
|
||||||
"no-return-await": 0,
|
"no-return-await": 0,
|
||||||
|
60
.github/workflows/codeql-analysis.yml
vendored
Normal file
60
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [dev, master]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [dev]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
# Override automatic language detection by changing the below list
|
||||||
|
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||||
|
language: ['javascript']
|
||||||
|
# Learn more...
|
||||||
|
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
# We must fetch at least the immediate parents so that if this is
|
||||||
|
# a pull request then we can checkout the head.
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
# If this run was triggered by a pull request event, then checkout
|
||||||
|
# the head of the pull request instead of the merge commit.
|
||||||
|
- run: git checkout HEAD^2
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
@@ -147,6 +147,10 @@
|
|||||||
"path": "M21.11,18.5C20.97,18.5 20.83,18.44 20.71,18.36C20.37,18.13 20.28,17.68 20.5,17.34C21.18,16.34 21.54,15.16 21.54,13.93C21.54,12.71 21.18,11.53 20.5,10.5C20.28,10.18 20.37,9.73 20.71,9.5C21.04,9.28 21.5,9.37 21.72,9.7C22.56,10.95 23,12.41 23,13.93C23,15.45 22.56,16.91 21.72,18.16C21.58,18.37 21.35,18.5 21.11,18.5M19,17.29C18.88,17.29 18.74,17.25 18.61,17.17C18.28,16.94 18.19,16.5 18.42,16.15C18.86,15.5 19.1,14.73 19.1,13.93C19.1,13.14 18.86,12.37 18.42,11.71C18.19,11.37 18.28,10.92 18.61,10.69C18.95,10.47 19.4,10.55 19.63,10.89C20.24,11.79 20.56,12.84 20.56,13.93C20.56,15 20.24,16.07 19.63,16.97C19.5,17.18 19.25,17.29 19,17.29M14.9,15.73C15.89,15.73 16.7,14.92 16.7,13.93C16.7,13.17 16.22,12.5 15.55,12.25C15.5,12.55 15.43,12.85 15.34,13.14C15.23,13.44 14.95,13.64 14.64,13.64C14.57,13.64 14.5,13.62 14.41,13.6C14.03,13.47 13.82,13.06 13.95,12.67C14.09,12.24 14.17,11.78 14.17,11.32C14.17,8.93 12.22,7 9.82,7C8.1,7 6.56,8 5.87,9.5C6.54,9.7 7.16,10.04 7.66,10.54C7.95,10.83 7.95,11.29 7.66,11.58C7.38,11.86 6.91,11.86 6.63,11.58C6.17,11.12 5.56,10.86 4.9,10.86C3.56,10.86 2.46,11.96 2.46,13.3C2.46,14.64 3.56,15.73 4.9,15.73H14.9M15.6,10.75C17.06,11.07 18.17,12.37 18.17,13.93C18.17,15.73 16.7,17.19 14.9,17.19H4.9C2.75,17.19 1,15.45 1,13.3C1,11.34 2.45,9.73 4.33,9.45C5.12,7.12 7.33,5.5 9.82,5.5C12.83,5.5 15.31,7.82 15.6,10.75Z",
|
"path": "M21.11,18.5C20.97,18.5 20.83,18.44 20.71,18.36C20.37,18.13 20.28,17.68 20.5,17.34C21.18,16.34 21.54,15.16 21.54,13.93C21.54,12.71 21.18,11.53 20.5,10.5C20.28,10.18 20.37,9.73 20.71,9.5C21.04,9.28 21.5,9.37 21.72,9.7C22.56,10.95 23,12.41 23,13.93C23,15.45 22.56,16.91 21.72,18.16C21.58,18.37 21.35,18.5 21.11,18.5M19,17.29C18.88,17.29 18.74,17.25 18.61,17.17C18.28,16.94 18.19,16.5 18.42,16.15C18.86,15.5 19.1,14.73 19.1,13.93C19.1,13.14 18.86,12.37 18.42,11.71C18.19,11.37 18.28,10.92 18.61,10.69C18.95,10.47 19.4,10.55 19.63,10.89C20.24,11.79 20.56,12.84 20.56,13.93C20.56,15 20.24,16.07 19.63,16.97C19.5,17.18 19.25,17.29 19,17.29M14.9,15.73C15.89,15.73 16.7,14.92 16.7,13.93C16.7,13.17 16.22,12.5 15.55,12.25C15.5,12.55 15.43,12.85 15.34,13.14C15.23,13.44 14.95,13.64 14.64,13.64C14.57,13.64 14.5,13.62 14.41,13.6C14.03,13.47 13.82,13.06 13.95,12.67C14.09,12.24 14.17,11.78 14.17,11.32C14.17,8.93 12.22,7 9.82,7C8.1,7 6.56,8 5.87,9.5C6.54,9.7 7.16,10.04 7.66,10.54C7.95,10.83 7.95,11.29 7.66,11.58C7.38,11.86 6.91,11.86 6.63,11.58C6.17,11.12 5.56,10.86 4.9,10.86C3.56,10.86 2.46,11.96 2.46,13.3C2.46,14.64 3.56,15.73 4.9,15.73H14.9M15.6,10.75C17.06,11.07 18.17,12.37 18.17,13.93C18.17,15.73 16.7,17.19 14.9,17.19H4.9C2.75,17.19 1,15.45 1,13.3C1,11.34 2.45,9.73 4.33,9.45C5.12,7.12 7.33,5.5 9.82,5.5C12.83,5.5 15.31,7.82 15.6,10.75Z",
|
||||||
"name": "mixcloud"
|
"name": "mixcloud"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "M5.68,3.96L11.41,11.65C11.55,11.84 11.55,12.1 11.41,12.29L5.65,20L5.5,20.18C4.76,21 3.47,21.07 2.64,20.31C1.85,19.59 1.79,18.37 2.43,17.5L6.56,11.97L2.46,6.47C1.83,5.62 1.88,4.39 2.67,3.67L2.82,3.54C3.73,2.87 5,3.05 5.68,3.96M18.32,3.96C19,3.05 20.27,2.87 21.18,3.54L21.33,3.67C22.12,4.39 22.17,5.61 21.54,6.47L17.44,11.97L21.57,17.5C22.21,18.36 22.15,19.59 21.36,20.31C20.53,21.07 19.24,21 18.5,20.18L18.35,20L12.59,12.29C12.45,12.1 12.45,11.84 12.59,11.65L18.32,3.96Z",
|
||||||
|
"name": "mixer"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "M3.25,4.03L19.95,20.73L18.7,22L14.86,18.13C14.77,18.12 14.68,18.09 14.59,18.05C14.26,17.89 14.14,17.62 14.11,17.38L12.18,15.45C12.14,15.53 12.09,15.6 12.05,15.66C11.62,16.26 11.19,16.26 10.86,16.04C10.54,15.83 5.5,12 5.23,11.87C4.95,11.76 4.85,12.03 5.12,13.5C5.39,15 4.95,15.39 4.57,15.45C4.2,15.5 3.06,15.18 3,12.14C2.95,9.11 3.76,8.62 4.14,8.62C4.6,8.62 7.08,10.69 8.84,12.12L2,5.28L3.25,4.03M18.38,16.56C18.75,15.4 19.12,13.8 19.1,12.03V12C19.14,8.5 17.66,5.58 17.66,5.58C17.66,5.58 17.42,4.72 18.12,4.39C18.83,4.06 19.3,4.61 19.3,4.61C21.12,8.22 21,11.64 21,12C21,12.27 21.09,14.96 19.88,18.05L18.38,16.56M15.14,13.31C15.19,12.92 15.22,12.5 15.24,12.03V12C15.14,8.5 14.13,7.21 14.13,7.21C14.13,7.21 13.89,6.34 14.59,6C15.3,5.69 15.77,6.23 15.77,6.23C17.26,8.94 17.16,11.64 17.14,12C17.15,12.2 17.2,13.38 16.82,15L15.14,13.31M10.2,8.38C10.23,7.77 10.59,7.64 10.59,7.64C10.59,7.64 11.19,7.37 11.57,7.8C11.91,8.19 12.72,9.57 12.89,11.07L10.2,8.38Z",
|
"path": "M3.25,4.03L19.95,20.73L18.7,22L14.86,18.13C14.77,18.12 14.68,18.09 14.59,18.05C14.26,17.89 14.14,17.62 14.11,17.38L12.18,15.45C12.14,15.53 12.09,15.6 12.05,15.66C11.62,16.26 11.19,16.26 10.86,16.04C10.54,15.83 5.5,12 5.23,11.87C4.95,11.76 4.85,12.03 5.12,13.5C5.39,15 4.95,15.39 4.57,15.45C4.2,15.5 3.06,15.18 3,12.14C2.95,9.11 3.76,8.62 4.14,8.62C4.6,8.62 7.08,10.69 8.84,12.12L2,5.28L3.25,4.03M18.38,16.56C18.75,15.4 19.12,13.8 19.1,12.03V12C19.14,8.5 17.66,5.58 17.66,5.58C17.66,5.58 17.42,4.72 18.12,4.39C18.83,4.06 19.3,4.61 19.3,4.61C21.12,8.22 21,11.64 21,12C21,12.27 21.09,14.96 19.88,18.05L18.38,16.56M15.14,13.31C15.19,12.92 15.22,12.5 15.24,12.03V12C15.14,8.5 14.13,7.21 14.13,7.21C14.13,7.21 13.89,6.34 14.59,6C15.3,5.69 15.77,6.23 15.77,6.23C17.26,8.94 17.16,11.64 17.14,12C17.15,12.2 17.2,13.38 16.82,15L15.14,13.31M10.2,8.38C10.23,7.77 10.59,7.64 10.59,7.64C10.59,7.64 11.19,7.37 11.57,7.8C11.91,8.19 12.72,9.57 12.89,11.07L10.2,8.38Z",
|
||||||
"name": "nfc-off"
|
"name": "nfc-off"
|
||||||
|
@@ -22,6 +22,8 @@ class HcLovelace extends LitElement {
|
|||||||
|
|
||||||
@property() public viewPath?: string | number;
|
@property() public viewPath?: string | number;
|
||||||
|
|
||||||
|
public urlPath?: string | null;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const index = this._viewIndex;
|
const index = this._viewIndex;
|
||||||
if (index === undefined) {
|
if (index === undefined) {
|
||||||
@@ -35,6 +37,7 @@ class HcLovelace extends LitElement {
|
|||||||
const lovelace: Lovelace = {
|
const lovelace: Lovelace = {
|
||||||
config: this.lovelaceConfig,
|
config: this.lovelaceConfig,
|
||||||
editMode: false,
|
editMode: false,
|
||||||
|
urlPath: this.urlPath!,
|
||||||
enableFullEditMode: () => undefined,
|
enableFullEditMode: () => undefined,
|
||||||
mode: "storage",
|
mode: "storage",
|
||||||
language: "en",
|
language: "en",
|
||||||
|
@@ -87,6 +87,7 @@ export class HcMain extends HassElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.lovelaceConfig=${this._lovelaceConfig}
|
.lovelaceConfig=${this._lovelaceConfig}
|
||||||
.viewPath=${this._lovelacePath}
|
.viewPath=${this._lovelacePath}
|
||||||
|
.urlPath=${this._urlPath}
|
||||||
@config-refresh=${this._generateLovelaceConfig}
|
@config-refresh=${this._generateLovelaceConfig}
|
||||||
></hc-lovelace>
|
></hc-lovelace>
|
||||||
`;
|
`;
|
||||||
|
@@ -52,7 +52,6 @@ class CastDemoRow extends LitElement implements LovelaceRow {
|
|||||||
});
|
});
|
||||||
mgr.castContext.addEventListener(
|
mgr.castContext.addEventListener(
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
// @ts-ignore
|
|
||||||
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
||||||
(ev) => {
|
(ev) => {
|
||||||
// On Android, opening a new session always results in SESSION_RESUMED.
|
// On Android, opening a new session always results in SESSION_RESUMED.
|
||||||
|
@@ -2,8 +2,8 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|||||||
/* eslint-plugin-disable lit */
|
/* eslint-plugin-disable lit */
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
import "../../../src/components/ha-card";
|
import "../../../src/components/ha-card";
|
||||||
import "../../../src/dialogs/more-info/more-info-content";
|
|
||||||
import "../../../src/state-summary/state-card-content";
|
import "../../../src/state-summary/state-card-content";
|
||||||
|
import "./more-info-content";
|
||||||
|
|
||||||
class DemoMoreInfo extends PolymerElement {
|
class DemoMoreInfo extends PolymerElement {
|
||||||
static get template() {
|
static get template() {
|
||||||
|
73
gallery/src/components/more-info-content.ts
Normal file
73
gallery/src/components/more-info-content.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { property, PropertyValues, UpdatingElement } from "lit-element";
|
||||||
|
import dynamicContentUpdater from "../../../src/common/dom/dynamic_content_updater";
|
||||||
|
import { stateMoreInfoType } from "../../../src/common/entity/state_more_info_type";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-alarm_control_panel";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-automation";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-camera";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-climate";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-configurator";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-counter";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-cover";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-default";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-fan";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-group";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-humidifier";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-input_datetime";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-light";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-lock";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-media_player";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-person";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-script";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-sun";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-timer";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-vacuum";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-water_heater";
|
||||||
|
import "../../../src/dialogs/more-info/controls/more-info-weather";
|
||||||
|
import { HomeAssistant } from "../../../src/types";
|
||||||
|
|
||||||
|
class MoreInfoContent extends UpdatingElement {
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public stateObj?: HassEntity;
|
||||||
|
|
||||||
|
private _detachedChild?: ChildNode;
|
||||||
|
|
||||||
|
protected firstUpdated(): void {
|
||||||
|
this.style.position = "relative";
|
||||||
|
this.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is not a lit element, but an updating element, so we implement update
|
||||||
|
protected update(changedProps: PropertyValues): void {
|
||||||
|
super.update(changedProps);
|
||||||
|
const stateObj = this.stateObj;
|
||||||
|
const hass = this.hass;
|
||||||
|
|
||||||
|
if (!stateObj || !hass) {
|
||||||
|
if (this.lastChild) {
|
||||||
|
this._detachedChild = this.lastChild;
|
||||||
|
// Detach child to prevent it from doing work.
|
||||||
|
this.removeChild(this.lastChild);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._detachedChild) {
|
||||||
|
this.appendChild(this._detachedChild);
|
||||||
|
this._detachedChild = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moreInfoType =
|
||||||
|
stateObj.attributes && "custom_ui_more_info" in stateObj.attributes
|
||||||
|
? stateObj.attributes.custom_ui_more_info
|
||||||
|
: "more-info-" + stateMoreInfoType(stateObj);
|
||||||
|
|
||||||
|
dynamicContentUpdater(this, moreInfoType.toUpperCase(), {
|
||||||
|
hass,
|
||||||
|
stateObj,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("more-info-content", MoreInfoContent);
|
@@ -3,10 +3,10 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
import "../../../src/components/ha-card";
|
import "../../../src/components/ha-card";
|
||||||
import { SUPPORT_BRIGHTNESS } from "../../../src/data/light";
|
import { SUPPORT_BRIGHTNESS } from "../../../src/data/light";
|
||||||
import "../../../src/dialogs/more-info/more-info-content";
|
|
||||||
import { getEntity } from "../../../src/fake_data/entity";
|
import { getEntity } from "../../../src/fake_data/entity";
|
||||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||||
import "../components/demo-more-infos";
|
import "../components/demo-more-infos";
|
||||||
|
import "../components/more-info-content";
|
||||||
|
|
||||||
const ENTITIES = [
|
const ENTITIES = [
|
||||||
getEntity("light", "bed_light", "on", {
|
getEntity("light", "bed_light", "on", {
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
import "@material/mwc-icon-button/mwc-icon-button";
|
import "@material/mwc-icon-button/mwc-icon-button";
|
||||||
|
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import { mdiDotsVertical } from "@mdi/js";
|
import { mdiDotsVertical } from "@mdi/js";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResult,
|
CSSResult,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { html, TemplateResult } from "lit-html";
|
import { html, TemplateResult } from "lit-html";
|
||||||
@@ -19,8 +20,9 @@ import {
|
|||||||
HassioAddonRepository,
|
HassioAddonRepository,
|
||||||
reloadHassioAddons,
|
reloadHassioAddons,
|
||||||
} from "../../../src/data/hassio/addon";
|
} from "../../../src/data/hassio/addon";
|
||||||
import "../../../src/layouts/hass-tabs-subpage";
|
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||||
import "../../../src/layouts/hass-loading-screen";
|
import "../../../src/layouts/hass-loading-screen";
|
||||||
|
import "../../../src/layouts/hass-tabs-subpage";
|
||||||
import { HomeAssistant, Route } from "../../../src/types";
|
import { HomeAssistant, Route } from "../../../src/types";
|
||||||
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
|
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
|
||||||
import { supervisorTabs } from "../hassio-tabs";
|
import { supervisorTabs } from "../hassio-tabs";
|
||||||
@@ -97,14 +99,18 @@ class HassioAddonStore extends LitElement {
|
|||||||
.tabs=${supervisorTabs}
|
.tabs=${supervisorTabs}
|
||||||
>
|
>
|
||||||
<span slot="header">Add-on store</span>
|
<span slot="header">Add-on store</span>
|
||||||
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
|
<ha-button-menu
|
||||||
|
corner="BOTTOM_START"
|
||||||
|
slot="toolbar-icon"
|
||||||
|
@action=${this._handleAction}
|
||||||
|
>
|
||||||
<mwc-icon-button slot="trigger" alt="menu">
|
<mwc-icon-button slot="trigger" alt="menu">
|
||||||
<ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
|
<ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
|
||||||
</mwc-icon-button>
|
</mwc-icon-button>
|
||||||
<mwc-list-item @tap=${this._manageRepositories}>
|
<mwc-list-item>
|
||||||
Repositories
|
Repositories
|
||||||
</mwc-list-item>
|
</mwc-list-item>
|
||||||
<mwc-list-item @tap=${this.refreshData}>
|
<mwc-list-item>
|
||||||
Reload
|
Reload
|
||||||
</mwc-list-item>
|
</mwc-list-item>
|
||||||
</ha-button-menu>
|
</ha-button-menu>
|
||||||
@@ -143,6 +149,17 @@ class HassioAddonStore extends LitElement {
|
|||||||
this._loadData();
|
this._loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||||
|
switch (ev.detail.index) {
|
||||||
|
case 0:
|
||||||
|
this._manageRepositories();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
this.refreshData();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private apiCalled(ev) {
|
private apiCalled(ev) {
|
||||||
if (ev.detail.success) {
|
if (ev.detail.success) {
|
||||||
this._loadData();
|
this._loadData();
|
||||||
@@ -163,7 +180,7 @@ class HassioAddonStore extends LitElement {
|
|||||||
this._repos.sort(sortRepos);
|
this._repos.sort(sortRepos);
|
||||||
this._addons = addonsInfo.addons;
|
this._addons = addonsInfo.addons;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Failed to fetch add-on info");
|
alert(extractApiErrorMessage(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -28,6 +28,7 @@ import { haStyle } from "../../../../src/resources/styles";
|
|||||||
import { HomeAssistant } from "../../../../src/types";
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
||||||
import { hassioStyle } from "../../resources/hassio-style";
|
import { hassioStyle } from "../../resources/hassio-style";
|
||||||
|
import "../../../../src/components/buttons/ha-progress-button";
|
||||||
|
|
||||||
@customElement("hassio-addon-audio")
|
@customElement("hassio-addon-audio")
|
||||||
class HassioAddonAudio extends LitElement {
|
class HassioAddonAudio extends LitElement {
|
||||||
@@ -91,7 +92,9 @@ class HassioAddonAudio extends LitElement {
|
|||||||
</paper-dropdown-menu>
|
</paper-dropdown-menu>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<mwc-button @click=${this._saveSettings}>Save</mwc-button>
|
<ha-progress-button @click=${this._saveSettings}>
|
||||||
|
Save
|
||||||
|
</ha-progress-button>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
@@ -172,7 +175,10 @@ class HassioAddonAudio extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _saveSettings(): Promise<void> {
|
private async _saveSettings(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
const data: HassioAddonSetOptionParams = {
|
const data: HassioAddonSetOptionParams = {
|
||||||
audio_input:
|
audio_input:
|
||||||
@@ -182,12 +188,14 @@ class HassioAddonAudio extends LitElement {
|
|||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||||
|
if (this.addon?.state === "started") {
|
||||||
|
await suggestAddonRestart(this, this.hass, this.addon);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
this._error = "Failed to set addon audio device";
|
this._error = "Failed to set addon audio device";
|
||||||
}
|
}
|
||||||
if (!this._error && this.addon?.state === "started") {
|
|
||||||
await suggestAddonRestart(this, this.hass, this.addon);
|
button.progress = false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,14 +5,15 @@ import {
|
|||||||
CSSResult,
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
query,
|
query,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
|
import "../../../../src/components/buttons/ha-progress-button";
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
import "../../../../src/components/ha-yaml-editor";
|
import "../../../../src/components/ha-yaml-editor";
|
||||||
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
|
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
HassioAddonSetOptionParams,
|
HassioAddonSetOptionParams,
|
||||||
setHassioAddonOption,
|
setHassioAddonOption,
|
||||||
} from "../../../../src/data/hassio/addon";
|
} from "../../../../src/data/hassio/addon";
|
||||||
|
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||||
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||||
import { haStyle } from "../../../../src/resources/styles";
|
import { haStyle } from "../../../../src/resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../src/types";
|
import type { HomeAssistant } from "../../../../src/types";
|
||||||
@@ -55,20 +57,103 @@ class HassioAddonConfig extends LitElement {
|
|||||||
${valid ? "" : html` <div class="errors">Invalid YAML</div> `}
|
${valid ? "" : html` <div class="errors">Invalid YAML</div> `}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<mwc-button class="warning" @click=${this._resetTapped}>
|
<ha-progress-button class="warning" @click=${this._resetTapped}>
|
||||||
Reset to defaults
|
Reset to defaults
|
||||||
</mwc-button>
|
</ha-progress-button>
|
||||||
<mwc-button
|
<ha-progress-button
|
||||||
@click=${this._saveTapped}
|
@click=${this._saveTapped}
|
||||||
.disabled=${!this._configHasChanged || !valid}
|
.disabled=${!this._configHasChanged || !valid}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</mwc-button>
|
</ha-progress-button>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected updated(changedProperties: PropertyValues): void {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
if (changedProperties.has("addon")) {
|
||||||
|
this._editor.setValue(this.addon.options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _configChanged(): void {
|
||||||
|
this._configHasChanged = true;
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _resetTapped(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
|
title: this.addon.name,
|
||||||
|
text: "Are you sure you want to reset all your options?",
|
||||||
|
confirmText: "reset options",
|
||||||
|
dismissText: "no",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
button.progress = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._error = undefined;
|
||||||
|
const data: HassioAddonSetOptionParams = {
|
||||||
|
options: null,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||||
|
this._configHasChanged = false;
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "options",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to reset addon configuration, ${extractApiErrorMessage(
|
||||||
|
err
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
button.progress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _saveTapped(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
|
let data: HassioAddonSetOptionParams;
|
||||||
|
this._error = undefined;
|
||||||
|
try {
|
||||||
|
data = {
|
||||||
|
options: this._editor.value,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
this._error = err;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||||
|
this._configHasChanged = false;
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "options",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
if (this.addon?.state === "started") {
|
||||||
|
await suggestAddonRestart(this, this.hass, this.addon);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to save addon configuration, ${extractApiErrorMessage(
|
||||||
|
err
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
button.progress = false;
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
@@ -98,80 +183,6 @@ class HassioAddonConfig extends LitElement {
|
|||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProperties: PropertyValues): void {
|
|
||||||
super.updated(changedProperties);
|
|
||||||
if (changedProperties.has("addon")) {
|
|
||||||
this._editor.setValue(this.addon.options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _configChanged(): void {
|
|
||||||
this._configHasChanged = true;
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _resetTapped(): Promise<void> {
|
|
||||||
const confirmed = await showConfirmationDialog(this, {
|
|
||||||
title: this.addon.name,
|
|
||||||
text: "Are you sure you want to reset all your options?",
|
|
||||||
confirmText: "reset options",
|
|
||||||
dismissText: "no",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._error = undefined;
|
|
||||||
const data: HassioAddonSetOptionParams = {
|
|
||||||
options: null,
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
|
||||||
this._configHasChanged = false;
|
|
||||||
const eventdata = {
|
|
||||||
success: true,
|
|
||||||
response: undefined,
|
|
||||||
path: "options",
|
|
||||||
};
|
|
||||||
fireEvent(this, "hass-api-called", eventdata);
|
|
||||||
} catch (err) {
|
|
||||||
this._error = `Failed to reset addon configuration, ${
|
|
||||||
err.body?.message || err
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _saveTapped(): Promise<void> {
|
|
||||||
let data: HassioAddonSetOptionParams;
|
|
||||||
this._error = undefined;
|
|
||||||
try {
|
|
||||||
data = {
|
|
||||||
options: this._editor.value,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
this._error = err;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
|
||||||
this._configHasChanged = false;
|
|
||||||
const eventdata = {
|
|
||||||
success: true,
|
|
||||||
response: undefined,
|
|
||||||
path: "options",
|
|
||||||
};
|
|
||||||
fireEvent(this, "hass-api-called", eventdata);
|
|
||||||
} catch (err) {
|
|
||||||
this._error = `Failed to save addon configuration, ${
|
|
||||||
err.body?.message || err
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
if (!this._error && this.addon?.state === "started") {
|
|
||||||
await suggestAddonRestart(this, this.hass, this.addon);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -4,19 +4,21 @@ import {
|
|||||||
CSSResult,
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
|
import "../../../../src/components/buttons/ha-progress-button";
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
import {
|
import {
|
||||||
HassioAddonDetails,
|
HassioAddonDetails,
|
||||||
HassioAddonSetOptionParams,
|
HassioAddonSetOptionParams,
|
||||||
setHassioAddonOption,
|
setHassioAddonOption,
|
||||||
} from "../../../../src/data/hassio/addon";
|
} from "../../../../src/data/hassio/addon";
|
||||||
|
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||||
import { haStyle } from "../../../../src/resources/styles";
|
import { haStyle } from "../../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../../src/types";
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
||||||
@@ -85,38 +87,17 @@ class HassioAddonNetwork extends LitElement {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<mwc-button class="warning" @click=${this._resetTapped}>
|
<ha-progress-button class="warning" @click=${this._resetTapped}>
|
||||||
Reset to defaults
|
Reset to defaults
|
||||||
</mwc-button>
|
</ha-progress-button>
|
||||||
<mwc-button @click=${this._saveTapped}>Save</mwc-button>
|
<ha-progress-button @click=${this._saveTapped}>
|
||||||
|
Save
|
||||||
|
</ha-progress-button>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
|
||||||
return [
|
|
||||||
haStyle,
|
|
||||||
hassioStyle,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
ha-card {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.errors {
|
|
||||||
color: var(--error-color);
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.card-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected update(changedProperties: PropertyValues): void {
|
protected update(changedProperties: PropertyValues): void {
|
||||||
super.update(changedProperties);
|
super.update(changedProperties);
|
||||||
if (changedProperties.has("addon")) {
|
if (changedProperties.has("addon")) {
|
||||||
@@ -149,7 +130,10 @@ class HassioAddonNetwork extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _resetTapped(): Promise<void> {
|
private async _resetTapped(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
const data: HassioAddonSetOptionParams = {
|
const data: HassioAddonSetOptionParams = {
|
||||||
network: null,
|
network: null,
|
||||||
};
|
};
|
||||||
@@ -162,17 +146,22 @@ class HassioAddonNetwork extends LitElement {
|
|||||||
path: "option",
|
path: "option",
|
||||||
};
|
};
|
||||||
fireEvent(this, "hass-api-called", eventdata);
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
if (this.addon?.state === "started") {
|
||||||
|
await suggestAddonRestart(this, this.hass, this.addon);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = `Failed to set addon network configuration, ${
|
this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
|
||||||
err.body?.message || err
|
err
|
||||||
}`;
|
)}`;
|
||||||
}
|
|
||||||
if (!this._error && this.addon?.state === "started") {
|
|
||||||
await suggestAddonRestart(this, this.hass, this.addon);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.progress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _saveTapped(): Promise<void> {
|
private async _saveTapped(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
const networkconfiguration = {};
|
const networkconfiguration = {};
|
||||||
this._config!.forEach((item) => {
|
this._config!.forEach((item) => {
|
||||||
@@ -191,14 +180,38 @@ class HassioAddonNetwork extends LitElement {
|
|||||||
path: "option",
|
path: "option",
|
||||||
};
|
};
|
||||||
fireEvent(this, "hass-api-called", eventdata);
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
if (this.addon?.state === "started") {
|
||||||
|
await suggestAddonRestart(this, this.hass, this.addon);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = `Failed to set addon network configuration, ${
|
this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
|
||||||
err.body?.message || err
|
err
|
||||||
}`;
|
)}`;
|
||||||
}
|
|
||||||
if (!this._error && this.addon?.state === "started") {
|
|
||||||
await suggestAddonRestart(this, this.hass, this.addon);
|
|
||||||
}
|
}
|
||||||
|
button.progress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
hassioStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
ha-card {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.errors {
|
||||||
|
color: var(--error-color);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,18 +3,19 @@ import {
|
|||||||
CSSResult,
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
|
import "../../../../src/components/ha-circular-progress";
|
||||||
import "../../../../src/components/ha-markdown";
|
import "../../../../src/components/ha-markdown";
|
||||||
import {
|
import {
|
||||||
fetchHassioAddonDocumentation,
|
fetchHassioAddonDocumentation,
|
||||||
HassioAddonDetails,
|
HassioAddonDetails,
|
||||||
} from "../../../../src/data/hassio/addon";
|
} from "../../../../src/data/hassio/addon";
|
||||||
|
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||||
import "../../../../src/layouts/hass-loading-screen";
|
import "../../../../src/layouts/hass-loading-screen";
|
||||||
import "../../../../src/components/ha-circular-progress";
|
|
||||||
import { haStyle } from "../../../../src/resources/styles";
|
import { haStyle } from "../../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../../src/types";
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
import { hassioStyle } from "../../resources/hassio-style";
|
import { hassioStyle } from "../../resources/hassio-style";
|
||||||
@@ -80,9 +81,9 @@ class HassioAddonDocumentationDashboard extends LitElement {
|
|||||||
this.addon!.slug
|
this.addon!.slug
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = `Failed to get addon documentation, ${
|
this._error = `Failed to get addon documentation, ${extractApiErrorMessage(
|
||||||
err.body?.message || err
|
err
|
||||||
}`;
|
)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,21 +9,19 @@ import {
|
|||||||
mdiExclamationThick,
|
mdiExclamationThick,
|
||||||
mdiFlask,
|
mdiFlask,
|
||||||
mdiHomeAssistant,
|
mdiHomeAssistant,
|
||||||
mdiInformation,
|
|
||||||
mdiKey,
|
mdiKey,
|
||||||
mdiNetwork,
|
mdiNetwork,
|
||||||
mdiPound,
|
mdiPound,
|
||||||
mdiShield,
|
mdiShield,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import "@polymer/paper-tooltip/paper-tooltip";
|
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResult,
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { classMap } from "lit-html/directives/class-map";
|
import { classMap } from "lit-html/directives/class-map";
|
||||||
@@ -35,19 +33,27 @@ import "../../../../src/components/buttons/ha-progress-button";
|
|||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
import "../../../../src/components/ha-label-badge";
|
import "../../../../src/components/ha-label-badge";
|
||||||
import "../../../../src/components/ha-markdown";
|
import "../../../../src/components/ha-markdown";
|
||||||
|
import "../../../../src/components/ha-settings-row";
|
||||||
import "../../../../src/components/ha-svg-icon";
|
import "../../../../src/components/ha-svg-icon";
|
||||||
import "../../../../src/components/ha-switch";
|
import "../../../../src/components/ha-switch";
|
||||||
import {
|
import {
|
||||||
fetchHassioAddonChangelog,
|
fetchHassioAddonChangelog,
|
||||||
|
fetchHassioAddonInfo,
|
||||||
HassioAddonDetails,
|
HassioAddonDetails,
|
||||||
HassioAddonSetOptionParams,
|
HassioAddonSetOptionParams,
|
||||||
HassioAddonSetSecurityParams,
|
HassioAddonSetSecurityParams,
|
||||||
installHassioAddon,
|
installHassioAddon,
|
||||||
setHassioAddonOption,
|
setHassioAddonOption,
|
||||||
setHassioAddonSecurity,
|
setHassioAddonSecurity,
|
||||||
|
startHassioAddon,
|
||||||
uninstallHassioAddon,
|
uninstallHassioAddon,
|
||||||
|
validateHassioAddonOption,
|
||||||
} from "../../../../src/data/hassio/addon";
|
} from "../../../../src/data/hassio/addon";
|
||||||
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||||
|
import {
|
||||||
|
showAlertDialog,
|
||||||
|
showConfirmationDialog,
|
||||||
|
} from "../../../../src/dialogs/generic/show-dialog-box";
|
||||||
import { haStyle } from "../../../../src/resources/styles";
|
import { haStyle } from "../../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../../src/types";
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
import "../../components/hassio-card-content";
|
import "../../components/hassio-card-content";
|
||||||
@@ -127,8 +133,6 @@ class HassioAddonInfo extends LitElement {
|
|||||||
|
|
||||||
@internalProperty() private _error?: string;
|
@internalProperty() private _error?: string;
|
||||||
|
|
||||||
@property({ type: Boolean }) private _installing = false;
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
${this._computeUpdateAvailable
|
${this._computeUpdateAvailable
|
||||||
@@ -242,19 +246,23 @@ class HassioAddonInfo extends LitElement {
|
|||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
<div class="security">
|
<div class="security">
|
||||||
<ha-label-badge
|
${this.addon.stage !== "stable"
|
||||||
class=${classMap({
|
? html` <ha-label-badge
|
||||||
green: this.addon.stage === "stable",
|
class=${classMap({
|
||||||
yellow: this.addon.stage === "experimental",
|
yellow: this.addon.stage === "experimental",
|
||||||
red: this.addon.stage === "deprecated",
|
red: this.addon.stage === "deprecated",
|
||||||
})}
|
})}
|
||||||
@click=${this._showMoreInfo}
|
@click=${this._showMoreInfo}
|
||||||
id="stage"
|
id="stage"
|
||||||
label="stage"
|
label="stage"
|
||||||
description=""
|
description=""
|
||||||
>
|
>
|
||||||
<ha-svg-icon .path=${STAGE_ICON[this.addon.stage]}></ha-svg-icon>
|
<ha-svg-icon
|
||||||
</ha-label-badge>
|
.path=${STAGE_ICON[this.addon.stage]}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</ha-label-badge>`
|
||||||
|
: ""}
|
||||||
|
|
||||||
<ha-label-badge
|
<ha-label-badge
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
green: [5, 6].includes(Number(this.addon.rating)),
|
green: [5, 6].includes(Number(this.addon.rating)),
|
||||||
@@ -382,67 +390,94 @@ class HassioAddonInfo extends LitElement {
|
|||||||
|
|
||||||
${this.addon.version
|
${this.addon.version
|
||||||
? html`
|
? html`
|
||||||
<div class="state">
|
<div class="addon-options">
|
||||||
<div>Start on boot</div>
|
<ha-settings-row ?three-line=${this.narrow}>
|
||||||
<ha-switch
|
<span slot="heading">
|
||||||
@change=${this._startOnBootToggled}
|
Start on boot
|
||||||
.checked=${this.addon.boot === "auto"}
|
</span>
|
||||||
haptic
|
<span slot="description">
|
||||||
></ha-switch>
|
Make the add-on start during a system boot
|
||||||
</div>
|
</span>
|
||||||
${this.addon.auto_update || this.hass.userData?.showAdvanced
|
<ha-switch
|
||||||
? html`
|
@change=${this._startOnBootToggled}
|
||||||
<div class="state">
|
.checked=${this.addon.boot === "auto"}
|
||||||
<div>Auto update</div>
|
haptic
|
||||||
<ha-switch
|
></ha-switch>
|
||||||
@change=${this._autoUpdateToggled}
|
</ha-settings-row>
|
||||||
.checked=${this.addon.auto_update}
|
|
||||||
haptic
|
${this.addon.startup !== "once"
|
||||||
></ha-switch>
|
? html`
|
||||||
</div>
|
<ha-settings-row ?three-line=${this.narrow}>
|
||||||
`
|
<span slot="heading">
|
||||||
: ""}
|
Watchdog
|
||||||
${this.addon.ingress
|
|
||||||
? html`
|
|
||||||
<div class="state">
|
|
||||||
<div>Show in sidebar</div>
|
|
||||||
<ha-switch
|
|
||||||
@change=${this._panelToggled}
|
|
||||||
.checked=${this.addon.ingress_panel}
|
|
||||||
.disabled=${this._computeCannotIngressSidebar}
|
|
||||||
haptic
|
|
||||||
></ha-switch>
|
|
||||||
${this._computeCannotIngressSidebar
|
|
||||||
? html`
|
|
||||||
<span>
|
|
||||||
This option requires Home Assistant 0.92 or
|
|
||||||
later.
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${this._computeUsesProtectedOptions
|
|
||||||
? html`
|
|
||||||
<div class="state">
|
|
||||||
<div>
|
|
||||||
Protection mode
|
|
||||||
<span>
|
|
||||||
<ha-svg-icon path=${mdiInformation}></ha-svg-icon>
|
|
||||||
<paper-tooltip>
|
|
||||||
Grant the add-on elevated system access.
|
|
||||||
</paper-tooltip>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<span slot="description">
|
||||||
<ha-switch
|
This will start the add-on if it crashes
|
||||||
@change=${this._protectionToggled}
|
</span>
|
||||||
.checked=${this.addon.protected}
|
<ha-switch
|
||||||
haptic
|
@change=${this._watchdogToggled}
|
||||||
></ha-switch>
|
.checked=${this.addon.watchdog}
|
||||||
</div>
|
haptic
|
||||||
`
|
></ha-switch>
|
||||||
: ""}
|
</ha-settings-row>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this.addon.auto_update || this.hass.userData?.showAdvanced
|
||||||
|
? html`
|
||||||
|
<ha-settings-row ?three-line=${this.narrow}>
|
||||||
|
<span slot="heading">
|
||||||
|
Auto update
|
||||||
|
</span>
|
||||||
|
<span slot="description">
|
||||||
|
Auto update the add-on when there is a new version
|
||||||
|
available
|
||||||
|
</span>
|
||||||
|
<ha-switch
|
||||||
|
@change=${this._autoUpdateToggled}
|
||||||
|
.checked=${this.addon.auto_update}
|
||||||
|
haptic
|
||||||
|
></ha-switch>
|
||||||
|
</ha-settings-row>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this.addon.ingress
|
||||||
|
? html`
|
||||||
|
<ha-settings-row ?three-line=${this.narrow}>
|
||||||
|
<span slot="heading">
|
||||||
|
Show in sidebar
|
||||||
|
</span>
|
||||||
|
<span slot="description">
|
||||||
|
${this._computeCannotIngressSidebar
|
||||||
|
? "This option requires Home Assistant 0.92 or later."
|
||||||
|
: "Add this add-on to your sidebar"}
|
||||||
|
</span>
|
||||||
|
<ha-switch
|
||||||
|
@change=${this._panelToggled}
|
||||||
|
.checked=${this.addon.ingress_panel}
|
||||||
|
.disabled=${this._computeCannotIngressSidebar}
|
||||||
|
haptic
|
||||||
|
></ha-switch>
|
||||||
|
</ha-settings-row>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this._computeUsesProtectedOptions
|
||||||
|
? html`
|
||||||
|
<ha-settings-row ?three-line=${this.narrow}>
|
||||||
|
<span slot="heading">
|
||||||
|
Protection mode
|
||||||
|
</span>
|
||||||
|
<span slot="description">
|
||||||
|
Blocks elevated system access from the add-on
|
||||||
|
</span>
|
||||||
|
<ha-switch
|
||||||
|
@change=${this._protectionToggled}
|
||||||
|
.checked=${this.addon.protected}
|
||||||
|
haptic
|
||||||
|
></ha-switch>
|
||||||
|
</ha-settings-row>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
||||||
@@ -468,12 +503,9 @@ class HassioAddonInfo extends LitElement {
|
|||||||
</ha-call-api-button>
|
</ha-call-api-button>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<ha-call-api-button
|
<ha-progress-button @click=${this._startClicked}>
|
||||||
.hass=${this.hass}
|
|
||||||
.path="hassio/addons/${this.addon.slug}/start"
|
|
||||||
>
|
|
||||||
Start
|
Start
|
||||||
</ha-call-api-button>
|
</ha-progress-button>
|
||||||
`}
|
`}
|
||||||
${this._computeShowWebUI
|
${this._computeShowWebUI
|
||||||
? html`
|
? html`
|
||||||
@@ -497,12 +529,12 @@ class HassioAddonInfo extends LitElement {
|
|||||||
</mwc-button>
|
</mwc-button>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
<mwc-button
|
<ha-progress-button
|
||||||
class=" right warning"
|
class=" right warning"
|
||||||
@click=${this._uninstallClicked}
|
@click=${this._uninstallClicked}
|
||||||
>
|
>
|
||||||
Uninstall
|
Uninstall
|
||||||
</mwc-button>
|
</ha-progress-button>
|
||||||
${this.addon.build
|
${this.addon.build
|
||||||
? html`
|
? html`
|
||||||
<ha-call-api-button
|
<ha-call-api-button
|
||||||
@@ -524,8 +556,7 @@ class HassioAddonInfo extends LitElement {
|
|||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
<ha-progress-button
|
<ha-progress-button
|
||||||
.disabled=${!this.addon.available || this._installing}
|
.disabled=${!this.addon.available}
|
||||||
.progress=${this._installing}
|
|
||||||
@click=${this._installClicked}
|
@click=${this._installClicked}
|
||||||
>
|
>
|
||||||
Install
|
Install
|
||||||
@@ -548,137 +579,6 @@ class HassioAddonInfo extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
|
||||||
return [
|
|
||||||
haStyle,
|
|
||||||
hassioStyle,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
ha-card {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
ha-card.warning {
|
|
||||||
background-color: var(--error-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
ha-card.warning .card-header {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
ha-card.warning .card-content {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
ha-card.warning mwc-button {
|
|
||||||
--mdc-theme-primary: white !important;
|
|
||||||
}
|
|
||||||
.warning {
|
|
||||||
color: var(--error-color);
|
|
||||||
--mdc-theme-primary: var(--error-color);
|
|
||||||
}
|
|
||||||
.light-color {
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
.addon-header {
|
|
||||||
padding-left: 8px;
|
|
||||||
font-size: 24px;
|
|
||||||
color: var(--ha-card-header-color, --primary-text-color);
|
|
||||||
}
|
|
||||||
.addon-version {
|
|
||||||
float: right;
|
|
||||||
font-size: 15px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.errors {
|
|
||||||
color: var(--error-color);
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.description {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
img.logo {
|
|
||||||
max-height: 60px;
|
|
||||||
margin: 16px 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.state {
|
|
||||||
display: flex;
|
|
||||||
margin: 33px 0;
|
|
||||||
}
|
|
||||||
.state div {
|
|
||||||
width: 180px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.state ha-svg-icon {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
ha-switch {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
ha-svg-icon.running {
|
|
||||||
color: var(--paper-green-400);
|
|
||||||
}
|
|
||||||
ha-svg-icon.stopped {
|
|
||||||
color: var(--google-red-300);
|
|
||||||
}
|
|
||||||
ha-call-api-button {
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
.right {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
protection-enable mwc-button {
|
|
||||||
--mdc-theme-primary: white;
|
|
||||||
}
|
|
||||||
.description a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
.red {
|
|
||||||
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
|
|
||||||
}
|
|
||||||
.blue {
|
|
||||||
--ha-label-badge-color: var(--label-badge-blue, #039be5);
|
|
||||||
}
|
|
||||||
.green {
|
|
||||||
--ha-label-badge-color: var(--label-badge-green, #0da035);
|
|
||||||
}
|
|
||||||
.yellow {
|
|
||||||
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
|
|
||||||
}
|
|
||||||
.security {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.card-actions {
|
|
||||||
display: flow-root;
|
|
||||||
}
|
|
||||||
.security h3 {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
.security ha-label-badge {
|
|
||||||
cursor: pointer;
|
|
||||||
margin-right: 4px;
|
|
||||||
--ha-label-badge-padding: 8px 0 0 0;
|
|
||||||
}
|
|
||||||
.changelog {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
.changelog-link {
|
|
||||||
color: var(--primary-color);
|
|
||||||
text-decoration: underline;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
ha-markdown {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _computeHassioApi(): boolean {
|
private get _computeHassioApi(): boolean {
|
||||||
return (
|
return (
|
||||||
this.addon.hassio_api &&
|
this.addon.hassio_api &&
|
||||||
@@ -763,7 +663,29 @@ class HassioAddonInfo extends LitElement {
|
|||||||
};
|
};
|
||||||
fireEvent(this, "hass-api-called", eventdata);
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = `Failed to set addon option, ${err.body?.message || err}`;
|
this._error = `Failed to set addon option, ${extractApiErrorMessage(
|
||||||
|
err
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _watchdogToggled(): Promise<void> {
|
||||||
|
this._error = undefined;
|
||||||
|
const data: HassioAddonSetOptionParams = {
|
||||||
|
watchdog: !this.addon.watchdog,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "option",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to set addon option, ${extractApiErrorMessage(
|
||||||
|
err
|
||||||
|
)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,7 +703,9 @@ class HassioAddonInfo extends LitElement {
|
|||||||
};
|
};
|
||||||
fireEvent(this, "hass-api-called", eventdata);
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = `Failed to set addon option, ${err.body?.message || err}`;
|
this._error = `Failed to set addon option, ${extractApiErrorMessage(
|
||||||
|
err
|
||||||
|
)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,9 +723,9 @@ class HassioAddonInfo extends LitElement {
|
|||||||
};
|
};
|
||||||
fireEvent(this, "hass-api-called", eventdata);
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = `Failed to set addon security option, ${
|
this._error = `Failed to set addon security option, ${extractApiErrorMessage(
|
||||||
err.body?.message || err
|
err
|
||||||
}`;
|
)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -819,12 +743,13 @@ class HassioAddonInfo extends LitElement {
|
|||||||
};
|
};
|
||||||
fireEvent(this, "hass-api-called", eventdata);
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = `Failed to set addon option, ${err.body?.message || err}`;
|
this._error = `Failed to set addon option, ${extractApiErrorMessage(
|
||||||
|
err
|
||||||
|
)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _openChangelog(): Promise<void> {
|
private async _openChangelog(): Promise<void> {
|
||||||
this._error = undefined;
|
|
||||||
try {
|
try {
|
||||||
const content = await fetchHassioAddonChangelog(
|
const content = await fetchHassioAddonChangelog(
|
||||||
this.hass,
|
this.hass,
|
||||||
@@ -835,15 +760,17 @@ class HassioAddonInfo extends LitElement {
|
|||||||
content,
|
content,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = `Failed to get addon changelog, ${
|
showAlertDialog(this, {
|
||||||
err.body?.message || err
|
title: "Failed to get addon changelog",
|
||||||
}`;
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _installClicked(): Promise<void> {
|
private async _installClicked(ev: CustomEvent): Promise<void> {
|
||||||
this._error = undefined;
|
const button = ev.currentTarget as any;
|
||||||
this._installing = true;
|
button.progress = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await installHassioAddon(this.hass, this.addon.slug);
|
await installHassioAddon(this.hass, this.addon.slug);
|
||||||
const eventdata = {
|
const eventdata = {
|
||||||
@@ -853,12 +780,62 @@ class HassioAddonInfo extends LitElement {
|
|||||||
};
|
};
|
||||||
fireEvent(this, "hass-api-called", eventdata);
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = `Failed to install addon, ${err.body?.message || err}`;
|
showAlertDialog(this, {
|
||||||
|
title: "Failed to install addon",
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this._installing = false;
|
button.progress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _uninstallClicked(): Promise<void> {
|
private async _startClicked(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
try {
|
||||||
|
const validate = await validateHassioAddonOption(
|
||||||
|
this.hass,
|
||||||
|
this.addon.slug
|
||||||
|
);
|
||||||
|
if (!validate.data.valid) {
|
||||||
|
await showConfirmationDialog(this, {
|
||||||
|
title: "Failed to start addon - configruation validation faled!",
|
||||||
|
text: validate.data.message.split(" Got ")[0],
|
||||||
|
confirm: () => this._openConfiguration(),
|
||||||
|
confirmText: "Go to configruation",
|
||||||
|
dismissText: "Cancel",
|
||||||
|
});
|
||||||
|
button.progress = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: "Failed to validate addon configuration",
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
button.progress = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await startHassioAddon(this.hass, this.addon.slug);
|
||||||
|
this.addon = await fetchHassioAddonInfo(this.hass, this.addon.slug);
|
||||||
|
} catch (err) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: "Failed to start addon",
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
button.progress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openConfiguration(): void {
|
||||||
|
navigate(this, `/hassio/addon/${this.addon.slug}/config`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _uninstallClicked(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
const confirmed = await showConfirmationDialog(this, {
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
title: this.addon.name,
|
title: this.addon.name,
|
||||||
text: "Are you sure you want to uninstall this add-on?",
|
text: "Are you sure you want to uninstall this add-on?",
|
||||||
@@ -867,6 +844,7 @@ class HassioAddonInfo extends LitElement {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
|
button.progress = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -880,8 +858,152 @@ class HassioAddonInfo extends LitElement {
|
|||||||
};
|
};
|
||||||
fireEvent(this, "hass-api-called", eventdata);
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = `Failed to uninstall addon, ${err.body?.message || err}`;
|
showAlertDialog(this, {
|
||||||
|
title: "Failed to uninstall addon",
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
button.progress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
hassioStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
ha-card {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
ha-card.warning {
|
||||||
|
background-color: var(--error-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
ha-card.warning .card-header {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
ha-card.warning .card-content {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
ha-card.warning mwc-button {
|
||||||
|
--mdc-theme-primary: white !important;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
color: var(--error-color);
|
||||||
|
--mdc-theme-primary: var(--error-color);
|
||||||
|
}
|
||||||
|
.light-color {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.addon-header {
|
||||||
|
padding-left: 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--ha-card-header-color, --primary-text-color);
|
||||||
|
}
|
||||||
|
.addon-version {
|
||||||
|
float: right;
|
||||||
|
font-size: 15px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.errors {
|
||||||
|
color: var(--error-color);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
img.logo {
|
||||||
|
max-height: 60px;
|
||||||
|
margin: 16px 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-switch {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
ha-svg-icon.running {
|
||||||
|
color: var(--paper-green-400);
|
||||||
|
}
|
||||||
|
ha-svg-icon.stopped {
|
||||||
|
color: var(--google-red-300);
|
||||||
|
}
|
||||||
|
ha-call-api-button {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
protection-enable mwc-button {
|
||||||
|
--mdc-theme-primary: white;
|
||||||
|
}
|
||||||
|
.description a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.red {
|
||||||
|
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
|
||||||
|
}
|
||||||
|
.blue {
|
||||||
|
--ha-label-badge-color: var(--label-badge-blue, #039be5);
|
||||||
|
}
|
||||||
|
.green {
|
||||||
|
--ha-label-badge-color: var(--label-badge-green, #0da035);
|
||||||
|
}
|
||||||
|
.yellow {
|
||||||
|
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
|
||||||
|
}
|
||||||
|
.security {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
display: flow-root;
|
||||||
|
}
|
||||||
|
.security h3 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.security ha-label-badge {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 4px;
|
||||||
|
--ha-label-badge-padding: 8px 0 0 0;
|
||||||
|
}
|
||||||
|
.changelog {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
.changelog-link {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
ha-markdown {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
ha-settings-row {
|
||||||
|
padding: 0;
|
||||||
|
height: 54px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
ha-settings-row > span[slot="description"] {
|
||||||
|
white-space: normal;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
ha-settings-row[three-line] {
|
||||||
|
height: 74px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-options {
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.addon-options {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -4,9 +4,9 @@ import {
|
|||||||
CSSResult,
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
fetchHassioAddonLogs,
|
fetchHassioAddonLogs,
|
||||||
HassioAddonDetails,
|
HassioAddonDetails,
|
||||||
} from "../../../../src/data/hassio/addon";
|
} from "../../../../src/data/hassio/addon";
|
||||||
|
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||||
import { haStyle } from "../../../../src/resources/styles";
|
import { haStyle } from "../../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../../src/types";
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
import "../../components/hassio-ansi-to-html";
|
import "../../components/hassio-ansi-to-html";
|
||||||
@@ -75,7 +76,7 @@ class HassioAddonLogs extends LitElement {
|
|||||||
try {
|
try {
|
||||||
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
|
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = `Failed to get addon logs, ${err.body?.message || err}`;
|
this._error = `Failed to get addon logs, ${extractApiErrorMessage(err)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -21,7 +21,7 @@ interface State {
|
|||||||
class HassioAnsiToHtml extends LitElement {
|
class HassioAnsiToHtml extends LitElement {
|
||||||
@property() public content!: string;
|
@property() public content!: string;
|
||||||
|
|
||||||
public render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
return html`${this._parseTextToColoredPre(this.content)}`;
|
return html`${this._parseTextToColoredPre(this.content)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,19 +5,28 @@ import {
|
|||||||
CSSResult,
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import "../../../src/components/buttons/ha-call-api-button";
|
import "../../../src/components/buttons/ha-progress-button";
|
||||||
import "../../../src/components/ha-card";
|
import "../../../src/components/ha-card";
|
||||||
import "../../../src/components/ha-svg-icon";
|
import "../../../src/components/ha-svg-icon";
|
||||||
|
import {
|
||||||
|
extractApiErrorMessage,
|
||||||
|
HassioResponse,
|
||||||
|
ignoredStatusCodes,
|
||||||
|
} from "../../../src/data/hassio/common";
|
||||||
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
|
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
|
||||||
import {
|
import {
|
||||||
HassioHomeAssistantInfo,
|
HassioHomeAssistantInfo,
|
||||||
HassioSupervisorInfo,
|
HassioSupervisorInfo,
|
||||||
} from "../../../src/data/hassio/supervisor";
|
} from "../../../src/data/hassio/supervisor";
|
||||||
|
import {
|
||||||
|
showAlertDialog,
|
||||||
|
showConfirmationDialog,
|
||||||
|
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||||
import { haStyle } from "../../../src/resources/styles";
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../src/types";
|
import { HomeAssistant } from "../../../src/types";
|
||||||
import { hassioStyle } from "../resources/hassio-style";
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
@@ -126,31 +135,46 @@ export class HassioUpdate extends LitElement {
|
|||||||
<a href="${releaseNotesUrl}" target="_blank" rel="noreferrer">
|
<a href="${releaseNotesUrl}" target="_blank" rel="noreferrer">
|
||||||
<mwc-button>Release notes</mwc-button>
|
<mwc-button>Release notes</mwc-button>
|
||||||
</a>
|
</a>
|
||||||
<ha-call-api-button
|
<ha-progress-button
|
||||||
.hass=${this.hass}
|
.apiPath=${apiPath}
|
||||||
.path=${apiPath}
|
.name=${name}
|
||||||
@hass-api-called=${this._apiCalled}
|
.version=${lastVersion}
|
||||||
|
@click=${this._confirmUpdate}
|
||||||
>
|
>
|
||||||
Update
|
Update
|
||||||
</ha-call-api-button>
|
</ha-progress-button>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _apiCalled(ev): void {
|
private async _confirmUpdate(ev): Promise<void> {
|
||||||
if (ev.detail.success) {
|
const item = ev.currentTarget;
|
||||||
this._error = "";
|
item.progress = true;
|
||||||
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
|
title: `Update ${item.name}`,
|
||||||
|
text: `Are you sure you want to upgrade ${item.name} to version ${item.version}?`,
|
||||||
|
confirmText: "update",
|
||||||
|
dismissText: "cancel",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
item.progress = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
const response = ev.detail.response;
|
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
|
||||||
|
} catch (err) {
|
||||||
if (typeof response.body === "object") {
|
// Only show an error if the status code was not expected (user behind proxy)
|
||||||
this._error = response.body.message || "Unknown error";
|
// or no status at all(connection terminated)
|
||||||
} else {
|
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
|
||||||
this._error = response.body;
|
showAlertDialog(this, {
|
||||||
|
title: "Update failed",
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
item.progress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
|
@@ -31,6 +31,10 @@ class HassioMarkdownDialog extends LitElement {
|
|||||||
this._opened = true;
|
this._opened = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public closeDialog() {
|
||||||
|
this._opened = false;
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this._opened) {
|
if (!this._opened) {
|
||||||
return html``;
|
return html``;
|
||||||
@@ -38,7 +42,7 @@ class HassioMarkdownDialog extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-dialog
|
||||||
open
|
open
|
||||||
@closing=${this._closeDialog}
|
@closed=${this.closeDialog}
|
||||||
.heading=${createCloseHeading(this.hass, this.title)}
|
.heading=${createCloseHeading(this.hass, this.title)}
|
||||||
>
|
>
|
||||||
<ha-markdown .content=${this.content || ""}></ha-markdown>
|
<ha-markdown .content=${this.content || ""}></ha-markdown>
|
||||||
@@ -46,10 +50,6 @@ class HassioMarkdownDialog extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _closeDialog(): void {
|
|
||||||
this._opened = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [
|
return [
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
|
333
hassio/src/dialogs/network/dialog-hassio-network.ts
Normal file
333
hassio/src/dialogs/network/dialog-hassio-network.ts
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
import "@material/mwc-button/mwc-button";
|
||||||
|
import "@material/mwc-icon-button";
|
||||||
|
import "@material/mwc-tab";
|
||||||
|
import "@material/mwc-tab-bar";
|
||||||
|
import { mdiClose } from "@mdi/js";
|
||||||
|
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
internalProperty,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
import { cache } from "lit-html/directives/cache";
|
||||||
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
|
import "../../../../src/components/ha-circular-progress";
|
||||||
|
import "../../../../src/components/ha-dialog";
|
||||||
|
import "../../../../src/components/ha-formfield";
|
||||||
|
import "../../../../src/components/ha-header-bar";
|
||||||
|
import "../../../../src/components/ha-radio";
|
||||||
|
import type { HaRadio } from "../../../../src/components/ha-radio";
|
||||||
|
import "../../../../src/components/ha-related-items";
|
||||||
|
import "../../../../src/components/ha-svg-icon";
|
||||||
|
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||||
|
import {
|
||||||
|
NetworkInterface,
|
||||||
|
updateNetworkInterface,
|
||||||
|
} from "../../../../src/data/hassio/network";
|
||||||
|
import {
|
||||||
|
showAlertDialog,
|
||||||
|
showConfirmationDialog,
|
||||||
|
} from "../../../../src/dialogs/generic/show-dialog-box";
|
||||||
|
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||||
|
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../../../src/types";
|
||||||
|
import { HassioNetworkDialogParams } from "./show-dialog-network";
|
||||||
|
|
||||||
|
@customElement("dialog-hassio-network")
|
||||||
|
export class DialogHassioNetwork extends LitElement implements HassDialog {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@internalProperty() private _prosessing = false;
|
||||||
|
|
||||||
|
@internalProperty() private _params?: HassioNetworkDialogParams;
|
||||||
|
|
||||||
|
@internalProperty() private _network!: {
|
||||||
|
interface: string;
|
||||||
|
data: NetworkInterface;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
@internalProperty() private _curTabIndex = 0;
|
||||||
|
|
||||||
|
@internalProperty() private _device?: {
|
||||||
|
interface: string;
|
||||||
|
data: NetworkInterface;
|
||||||
|
};
|
||||||
|
|
||||||
|
@internalProperty() private _dirty = false;
|
||||||
|
|
||||||
|
public async showDialog(params: HassioNetworkDialogParams): Promise<void> {
|
||||||
|
this._params = params;
|
||||||
|
this._dirty = false;
|
||||||
|
this._curTabIndex = 0;
|
||||||
|
this._network = Object.keys(params.network?.interfaces)
|
||||||
|
.map((device) => ({
|
||||||
|
interface: device,
|
||||||
|
data: params.network.interfaces[device],
|
||||||
|
}))
|
||||||
|
.sort((a, b) => {
|
||||||
|
return a.data.primary > b.data.primary ? -1 : 1;
|
||||||
|
});
|
||||||
|
this._device = this._network[this._curTabIndex];
|
||||||
|
this._device.data.nameservers = String(this._device.data.nameservers);
|
||||||
|
await this.updateComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog(): void {
|
||||||
|
this._params = undefined;
|
||||||
|
this._prosessing = false;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this._params || !this._network) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
scrimClickAction
|
||||||
|
escapeKeyAction
|
||||||
|
.heading=${true}
|
||||||
|
hideActions
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
>
|
||||||
|
<div slot="heading">
|
||||||
|
<ha-header-bar>
|
||||||
|
<span slot="title">
|
||||||
|
Network settings
|
||||||
|
</span>
|
||||||
|
<mwc-icon-button slot="actionItems" dialogAction="cancel">
|
||||||
|
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||||
|
</mwc-icon-button>
|
||||||
|
</ha-header-bar>
|
||||||
|
${this._network.length > 1
|
||||||
|
? html` <mwc-tab-bar
|
||||||
|
.activeIndex=${this._curTabIndex}
|
||||||
|
@MDCTabBar:activated=${this._handleTabActivated}
|
||||||
|
>${this._network.map(
|
||||||
|
(device) =>
|
||||||
|
html`<mwc-tab
|
||||||
|
.id=${device.interface}
|
||||||
|
.label=${device.interface}
|
||||||
|
>
|
||||||
|
</mwc-tab>`
|
||||||
|
)}
|
||||||
|
</mwc-tab-bar>`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
${cache(this._renderTab())}
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderTab() {
|
||||||
|
return html` <div class="form container">
|
||||||
|
<ha-formfield label="DHCP">
|
||||||
|
<ha-radio
|
||||||
|
@change=${this._handleRadioValueChanged}
|
||||||
|
value="dhcp"
|
||||||
|
name="method"
|
||||||
|
?checked=${this._device!.data.method === "dhcp"}
|
||||||
|
>
|
||||||
|
</ha-radio>
|
||||||
|
</ha-formfield>
|
||||||
|
<ha-formfield label="Static">
|
||||||
|
<ha-radio
|
||||||
|
@change=${this._handleRadioValueChanged}
|
||||||
|
value="static"
|
||||||
|
name="method"
|
||||||
|
?checked=${this._device!.data.method === "static"}
|
||||||
|
>
|
||||||
|
</ha-radio>
|
||||||
|
</ha-formfield>
|
||||||
|
${this._device!.data.method !== "dhcp"
|
||||||
|
? html` <paper-input
|
||||||
|
class="flex-auto"
|
||||||
|
id="ip_address"
|
||||||
|
label="IP address/Netmask"
|
||||||
|
.value="${this._device!.data.ip_address}"
|
||||||
|
@value-changed=${this._handleInputValueChanged}
|
||||||
|
></paper-input>
|
||||||
|
<paper-input
|
||||||
|
class="flex-auto"
|
||||||
|
id="gateway"
|
||||||
|
label="Gateway address"
|
||||||
|
.value="${this._device!.data.gateway}"
|
||||||
|
@value-changed=${this._handleInputValueChanged}
|
||||||
|
></paper-input>
|
||||||
|
<paper-input
|
||||||
|
class="flex-auto"
|
||||||
|
id="nameservers"
|
||||||
|
label="DNS servers"
|
||||||
|
.value="${this._device!.data.nameservers as string}"
|
||||||
|
@value-changed=${this._handleInputValueChanged}
|
||||||
|
></paper-input>
|
||||||
|
NB!: If you are changing IP or gateway addresses, you might lose
|
||||||
|
the connection.`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<mwc-button label="close" @click=${this.closeDialog}> </mwc-button>
|
||||||
|
<mwc-button @click=${this._updateNetwork} ?disabled=${!this._dirty}>
|
||||||
|
${this._prosessing
|
||||||
|
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||||
|
: "Update"}
|
||||||
|
</mwc-button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _updateNetwork() {
|
||||||
|
this._prosessing = true;
|
||||||
|
let options: Partial<NetworkInterface> = {
|
||||||
|
method: this._device!.data.method,
|
||||||
|
};
|
||||||
|
if (options.method !== "dhcp") {
|
||||||
|
options = {
|
||||||
|
...options,
|
||||||
|
address: this._device!.data.ip_address,
|
||||||
|
gateway: this._device!.data.gateway,
|
||||||
|
dns: String(this._device!.data.nameservers).split(","),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await updateNetworkInterface(this.hass, this._device!.interface, options);
|
||||||
|
} catch (err) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: "Failed to change network settings",
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
this._prosessing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._params?.loadData();
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleTabActivated(ev: CustomEvent): Promise<void> {
|
||||||
|
if (this._dirty) {
|
||||||
|
const confirm = await showConfirmationDialog(this, {
|
||||||
|
text:
|
||||||
|
"You have unsaved changes, these will get lost if you change tabs, do you want to continue?",
|
||||||
|
confirmText: "yes",
|
||||||
|
dismissText: "no",
|
||||||
|
});
|
||||||
|
if (!confirm) {
|
||||||
|
this.requestUpdate("_device");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._curTabIndex = ev.detail.index;
|
||||||
|
this._device = this._network[ev.detail.index];
|
||||||
|
this._device.data.nameservers = String(this._device.data.nameservers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleRadioValueChanged(ev: CustomEvent): void {
|
||||||
|
const value = (ev.target as HaRadio).value as "dhcp" | "static";
|
||||||
|
|
||||||
|
if (!value || !this._device || this._device!.data.method === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._dirty = true;
|
||||||
|
|
||||||
|
this._device!.data.method = value;
|
||||||
|
this.requestUpdate("_device");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleInputValueChanged(ev: CustomEvent): void {
|
||||||
|
const value: string | null | undefined = (ev.target as PaperInputElement)
|
||||||
|
.value;
|
||||||
|
const id = (ev.target as PaperInputElement).id;
|
||||||
|
|
||||||
|
if (!value || !this._device || this._device.data[id] === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._dirty = true;
|
||||||
|
|
||||||
|
this._device.data[id] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
ha-header-bar {
|
||||||
|
--mdc-theme-on-primary: var(--primary-text-color);
|
||||||
|
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
mwc-tab-bar {
|
||||||
|
border-bottom: 1px solid
|
||||||
|
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-dialog {
|
||||||
|
--dialog-content-position: static;
|
||||||
|
--dialog-content-padding: 0;
|
||||||
|
--dialog-z-index: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (min-width: 451px) and (min-height: 501px) {
|
||||||
|
.container {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: block;
|
||||||
|
padding: 20px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* overrule the ha-style-dialog max-height on small screens */
|
||||||
|
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||||
|
ha-header-bar {
|
||||||
|
--mdc-theme-primary: var(--app-header-background-color);
|
||||||
|
--mdc-theme-on-primary: var(--app-header-text-color, white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mwc-button.warning {
|
||||||
|
--mdc-theme-primary: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([rtl]) app-toolbar {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
padding: 20px 24px;
|
||||||
|
}
|
||||||
|
.form {
|
||||||
|
margin-bottom: 53px;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-top: 1px solid
|
||||||
|
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px;
|
||||||
|
padding-bottom: max(env(safe-area-inset-bottom), 8px);
|
||||||
|
background-color: var(--mdc-theme-surface, #fff);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"dialog-hassio-network": DialogHassioNetwork;
|
||||||
|
}
|
||||||
|
}
|
22
hassio/src/dialogs/network/show-dialog-network.ts
Normal file
22
hassio/src/dialogs/network/show-dialog-network.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
|
import { NetworkInfo } from "../../../../src/data/hassio/network";
|
||||||
|
import "./dialog-hassio-network";
|
||||||
|
|
||||||
|
export interface HassioNetworkDialogParams {
|
||||||
|
network: NetworkInfo;
|
||||||
|
loadData: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showNetworkDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
dialogParams: HassioNetworkDialogParams
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "dialog-hassio-network",
|
||||||
|
dialogImport: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "dialog-hassio-network" */ "./dialog-hassio-network"
|
||||||
|
),
|
||||||
|
dialogParams,
|
||||||
|
});
|
||||||
|
};
|
@@ -5,25 +5,26 @@ import "@polymer/paper-input/paper-input";
|
|||||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||||
import "@polymer/paper-item/paper-item";
|
import "@polymer/paper-item/paper-item";
|
||||||
import "@polymer/paper-item/paper-item-body";
|
import "@polymer/paper-item/paper-item-body";
|
||||||
import "../../../../src/components/ha-circular-progress";
|
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResult,
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
query,
|
query,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import "../../../../src/components/ha-circular-progress";
|
||||||
import "../../../../src/components/ha-dialog";
|
import "../../../../src/components/ha-dialog";
|
||||||
import "../../../../src/components/ha-svg-icon";
|
import "../../../../src/components/ha-svg-icon";
|
||||||
import {
|
import {
|
||||||
fetchHassioAddonsInfo,
|
fetchHassioAddonsInfo,
|
||||||
HassioAddonRepository,
|
HassioAddonRepository,
|
||||||
} from "../../../../src/data/hassio/addon";
|
} from "../../../../src/data/hassio/addon";
|
||||||
|
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||||
import { setSupervisorOption } from "../../../../src/data/hassio/supervisor";
|
import { setSupervisorOption } from "../../../../src/data/hassio/supervisor";
|
||||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../src/types";
|
import type { HomeAssistant } from "../../../../src/types";
|
||||||
@@ -190,7 +191,7 @@ class HassioRepositoriesDialog extends LitElement {
|
|||||||
|
|
||||||
input.value = "";
|
input.value = "";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = err.message;
|
this._error = extractApiErrorMessage(err);
|
||||||
}
|
}
|
||||||
this._prosessing = false;
|
this._prosessing = false;
|
||||||
}
|
}
|
||||||
@@ -222,7 +223,7 @@ class HassioRepositoriesDialog extends LitElement {
|
|||||||
|
|
||||||
await this._dialogParams!.loadData();
|
await this._dialogParams!.loadData();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = err.message;
|
this._error = extractApiErrorMessage(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,18 +7,20 @@ import {
|
|||||||
CSSResult,
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||||
import "../../../../src/components/ha-svg-icon";
|
import "../../../../src/components/ha-svg-icon";
|
||||||
import { getSignedPath } from "../../../../src/data/auth";
|
import { getSignedPath } from "../../../../src/data/auth";
|
||||||
|
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||||
import {
|
import {
|
||||||
fetchHassioSnapshotInfo,
|
fetchHassioSnapshotInfo,
|
||||||
HassioSnapshotDetail,
|
HassioSnapshotDetail,
|
||||||
} from "../../../../src/data/hassio/snapshot";
|
} from "../../../../src/data/hassio/snapshot";
|
||||||
|
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||||
import { PolymerChangedEvent } from "../../../../src/polymer-types";
|
import { PolymerChangedEvent } from "../../../../src/polymer-types";
|
||||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../../src/types";
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
@@ -266,8 +268,12 @@ class HassioSnapshotDialog extends LitElement {
|
|||||||
this._snapshotPassword = ev.detail.value;
|
this._snapshotPassword = ev.detail.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _partialRestoreClicked() {
|
private async _partialRestoreClicked() {
|
||||||
if (!confirm("Are you sure you want to restore this snapshot?")) {
|
if (
|
||||||
|
!(await showConfirmationDialog(this, {
|
||||||
|
title: "Are you sure you want partially to restore this snapshot?",
|
||||||
|
}))
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,8 +318,13 @@ class HassioSnapshotDialog extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _fullRestoreClicked() {
|
private async _fullRestoreClicked() {
|
||||||
if (!confirm("Are you sure you want to restore this snapshot?")) {
|
if (
|
||||||
|
!(await showConfirmationDialog(this, {
|
||||||
|
title:
|
||||||
|
"Are you sure you want to wipe your system and restore this snapshot?",
|
||||||
|
}))
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,8 +349,12 @@ class HassioSnapshotDialog extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _deleteClicked() {
|
private async _deleteClicked() {
|
||||||
if (!confirm("Are you sure you want to delete this snapshot?")) {
|
if (
|
||||||
|
!(await showConfirmationDialog(this, {
|
||||||
|
title: "Are you sure you want to delete this snapshot?",
|
||||||
|
}))
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +380,7 @@ class HassioSnapshotDialog extends LitElement {
|
|||||||
`/api/hassio/snapshots/${this._snapshot!.slug}/download`
|
`/api/hassio/snapshots/${this._snapshot!.slug}/download`
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(`Error: ${err.message}`);
|
alert(`Error: ${extractApiErrorMessage(err)}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ import {
|
|||||||
HassioAddonDetails,
|
HassioAddonDetails,
|
||||||
restartHassioAddon,
|
restartHassioAddon,
|
||||||
} from "../../../src/data/hassio/addon";
|
} from "../../../src/data/hassio/addon";
|
||||||
|
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||||
import {
|
import {
|
||||||
showAlertDialog,
|
showAlertDialog,
|
||||||
showConfirmationDialog,
|
showConfirmationDialog,
|
||||||
@@ -26,7 +27,7 @@ export const suggestAddonRestart = async (
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlertDialog(element, {
|
showAlertDialog(element, {
|
||||||
title: "Failed to restart",
|
title: "Failed to restart",
|
||||||
text: err.body.message,
|
text: extractApiErrorMessage(err),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,116 +1,35 @@
|
|||||||
import { PolymerElement } from "@polymer/polymer";
|
|
||||||
import {
|
import {
|
||||||
customElement,
|
html,
|
||||||
property,
|
|
||||||
internalProperty,
|
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
|
customElement,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
|
import "./hassio-router";
|
||||||
|
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
|
||||||
|
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||||
|
import { HomeAssistant, Route } from "../../src/types";
|
||||||
|
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||||
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||||
import { fireEvent } from "../../src/common/dom/fire_event";
|
import { fireEvent } from "../../src/common/dom/fire_event";
|
||||||
import { navigate } from "../../src/common/navigate";
|
|
||||||
import { fetchHassioAddonInfo } from "../../src/data/hassio/addon";
|
|
||||||
import {
|
|
||||||
fetchHassioHassOsInfo,
|
|
||||||
fetchHassioHostInfo,
|
|
||||||
HassioHassOSInfo,
|
|
||||||
HassioHostInfo,
|
|
||||||
} from "../../src/data/hassio/host";
|
|
||||||
import {
|
|
||||||
createHassioSession,
|
|
||||||
fetchHassioHomeAssistantInfo,
|
|
||||||
fetchHassioSupervisorInfo,
|
|
||||||
fetchHassioInfo,
|
|
||||||
HassioHomeAssistantInfo,
|
|
||||||
HassioInfo,
|
|
||||||
HassioPanelInfo,
|
|
||||||
HassioSupervisorInfo,
|
|
||||||
} from "../../src/data/hassio/supervisor";
|
|
||||||
import {
|
|
||||||
AlertDialogParams,
|
|
||||||
showAlertDialog,
|
|
||||||
} from "../../src/dialogs/generic/show-dialog-box";
|
|
||||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||||
import {
|
import { atLeastVersion } from "../../src/common/config/version";
|
||||||
HassRouterPage,
|
|
||||||
RouterOptions,
|
|
||||||
} from "../../src/layouts/hass-router-page";
|
|
||||||
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
|
||||||
import "../../src/resources/ha-style";
|
|
||||||
import { HomeAssistant } from "../../src/types";
|
|
||||||
// Don't codesplit it, that way the dashboard always loads fast.
|
|
||||||
import "./hassio-panel";
|
|
||||||
|
|
||||||
@customElement("hassio-main")
|
@customElement("hassio-main")
|
||||||
class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public panel!: HassioPanelInfo;
|
@property() public panel!: HassioPanelInfo;
|
||||||
|
|
||||||
@property() public narrow!: boolean;
|
@property() public narrow!: boolean;
|
||||||
|
|
||||||
protected routerOptions: RouterOptions = {
|
@property() public route?: Route;
|
||||||
// Hass.io has a page with tabs, so we route all non-matching routes to it.
|
|
||||||
defaultPage: "dashboard",
|
|
||||||
initialLoad: () => this._fetchData(),
|
|
||||||
showLoading: true,
|
|
||||||
routes: {
|
|
||||||
dashboard: {
|
|
||||||
tag: "hassio-panel",
|
|
||||||
cache: true,
|
|
||||||
},
|
|
||||||
snapshots: "dashboard",
|
|
||||||
store: "dashboard",
|
|
||||||
system: "dashboard",
|
|
||||||
addon: {
|
|
||||||
tag: "hassio-addon-dashboard",
|
|
||||||
load: () =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
ingress: {
|
|
||||||
tag: "hassio-ingress-view",
|
|
||||||
load: () =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
@internalProperty() private _supervisorInfo: HassioSupervisorInfo;
|
|
||||||
|
|
||||||
@internalProperty() private _hostInfo: HassioHostInfo;
|
|
||||||
|
|
||||||
@internalProperty() private _hassioInfo?: HassioInfo;
|
|
||||||
|
|
||||||
@internalProperty() private _hassOsInfo?: HassioHassOSInfo;
|
|
||||||
|
|
||||||
@internalProperty() private _hassInfo: HassioHomeAssistantInfo;
|
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues) {
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
|
|
||||||
applyThemesOnElement(
|
this._applyTheme();
|
||||||
this.parentElement,
|
|
||||||
this.hass.themes,
|
|
||||||
this.hass.selectedTheme || this.hass.themes.default_theme
|
|
||||||
);
|
|
||||||
|
|
||||||
this.style.setProperty(
|
|
||||||
"--app-header-background-color",
|
|
||||||
"var(--sidebar-background-color)"
|
|
||||||
);
|
|
||||||
this.style.setProperty(
|
|
||||||
"--app-header-text-color",
|
|
||||||
"var(--sidebar-text-color)"
|
|
||||||
);
|
|
||||||
this.style.setProperty(
|
|
||||||
"--app-header-border-bottom",
|
|
||||||
"1px solid var(--divider-color)"
|
|
||||||
);
|
|
||||||
|
|
||||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
|
||||||
// Paulus - March 17, 2019
|
// Paulus - March 17, 2019
|
||||||
// We went to a single hass-toggle-menu event in HA 0.90. However, the
|
// We went to a single hass-toggle-menu event in HA 0.90. However, the
|
||||||
// supervisor UI can also run under older versions of Home Assistant.
|
// supervisor UI can also run under older versions of Home Assistant.
|
||||||
@@ -143,152 +62,61 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
makeDialogManager(this, document.body);
|
makeDialogManager(this, this.shadowRoot!);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updatePageEl(el) {
|
protected updated(changedProps: PropertyValues) {
|
||||||
// the tabs page does its own routing so needs full route.
|
super.updated(changedProps);
|
||||||
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
|
if (!oldHass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (oldHass.themes !== this.hass.themes) {
|
||||||
|
this._applyTheme();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ("setProperties" in el) {
|
protected render() {
|
||||||
// As long as we have Polymer pages
|
return html`
|
||||||
(el as PolymerElement).setProperties({
|
<hassio-router
|
||||||
hass: this.hass,
|
.hass=${this.hass}
|
||||||
narrow: this.narrow,
|
.route=${this.route}
|
||||||
supervisorInfo: this._supervisorInfo,
|
.panel=${this.panel}
|
||||||
hassioInfo: this._hassioInfo,
|
.narrow=${this.narrow}
|
||||||
hostInfo: this._hostInfo,
|
></hassio-router>
|
||||||
hassInfo: this._hassInfo,
|
`;
|
||||||
hassOsInfo: this._hassOsInfo,
|
}
|
||||||
route,
|
|
||||||
});
|
private _applyTheme() {
|
||||||
|
let themeName: string;
|
||||||
|
let options: Partial<HomeAssistant["selectedTheme"]> | undefined;
|
||||||
|
|
||||||
|
if (atLeastVersion(this.hass.config.version, 0, 114)) {
|
||||||
|
themeName =
|
||||||
|
this.hass.selectedTheme?.theme ||
|
||||||
|
(this.hass.themes.darkMode && this.hass.themes.default_dark_theme
|
||||||
|
? this.hass.themes.default_dark_theme!
|
||||||
|
: this.hass.themes.default_theme);
|
||||||
|
|
||||||
|
options = this.hass.selectedTheme;
|
||||||
|
if (themeName === "default" && options?.dark === undefined) {
|
||||||
|
options = {
|
||||||
|
...this.hass.selectedTheme,
|
||||||
|
dark: this.hass.themes.darkMode,
|
||||||
|
};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
el.hass = this.hass;
|
themeName =
|
||||||
el.narrow = this.narrow;
|
((this.hass.selectedTheme as unknown) as string) ||
|
||||||
el.supervisorInfo = this._supervisorInfo;
|
this.hass.themes.default_theme;
|
||||||
el.hassioInfo = this._hassioInfo;
|
|
||||||
el.hostInfo = this._hostInfo;
|
|
||||||
el.hassInfo = this._hassInfo;
|
|
||||||
el.hassOsInfo = this._hassOsInfo;
|
|
||||||
el.route = route;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _fetchData() {
|
|
||||||
if (this.panel.config && this.panel.config.ingress) {
|
|
||||||
await this._redirectIngress(this.panel.config.ingress);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [supervisorInfo, hostInfo, hassInfo, hassioInfo] = await Promise.all([
|
applyThemesOnElement(
|
||||||
fetchHassioSupervisorInfo(this.hass),
|
this.parentElement,
|
||||||
fetchHassioHostInfo(this.hass),
|
this.hass.themes,
|
||||||
fetchHassioHomeAssistantInfo(this.hass),
|
themeName,
|
||||||
fetchHassioInfo(this.hass),
|
options
|
||||||
]);
|
|
||||||
this._supervisorInfo = supervisorInfo;
|
|
||||||
this._hassioInfo = hassioInfo;
|
|
||||||
this._hostInfo = hostInfo;
|
|
||||||
this._hassInfo = hassInfo;
|
|
||||||
|
|
||||||
if (this._hostInfo.features && this._hostInfo.features.includes("hassos")) {
|
|
||||||
this._hassOsInfo = await fetchHassioHassOsInfo(this.hass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _redirectIngress(addonSlug: string) {
|
|
||||||
// When we trigger a navigation, we sleep to make sure we don't
|
|
||||||
// show the hassio dashboard before navigating away.
|
|
||||||
const awaitAlert = async (
|
|
||||||
alertParams: AlertDialogParams,
|
|
||||||
action: () => void
|
|
||||||
) => {
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
alertParams.confirm = resolve;
|
|
||||||
showAlertDialog(this, alertParams);
|
|
||||||
});
|
|
||||||
action();
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
};
|
|
||||||
|
|
||||||
const createSessionPromise = createHassioSession(this.hass).then(
|
|
||||||
() => true,
|
|
||||||
() => false
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let addon;
|
|
||||||
|
|
||||||
try {
|
|
||||||
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
|
|
||||||
} catch (err) {
|
|
||||||
await awaitAlert(
|
|
||||||
{
|
|
||||||
text: "Unable to fetch add-on info to start Ingress",
|
|
||||||
title: "Supervisor",
|
|
||||||
},
|
|
||||||
() => history.back()
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!addon.ingress_url) {
|
|
||||||
await awaitAlert(
|
|
||||||
{
|
|
||||||
text: "Add-on does not support Ingress",
|
|
||||||
title: addon.name,
|
|
||||||
},
|
|
||||||
() => history.back()
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addon.state !== "started") {
|
|
||||||
await awaitAlert(
|
|
||||||
{
|
|
||||||
text: "Add-on is not running. Please start it first",
|
|
||||||
title: addon.name,
|
|
||||||
},
|
|
||||||
() => navigate(this, `/hassio/addon/${addon.slug}/info`, true)
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await createSessionPromise)) {
|
|
||||||
await awaitAlert(
|
|
||||||
{
|
|
||||||
text: "Unable to create an Ingress session",
|
|
||||||
title: addon.name,
|
|
||||||
},
|
|
||||||
() => history.back()
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
location.assign(addon.ingress_url);
|
|
||||||
// await a promise that doesn't resolve, so we show the loading screen
|
|
||||||
// while we load the next page.
|
|
||||||
await new Promise(() => undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _apiCalled(ev) {
|
|
||||||
if (!ev.detail.success) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tries = 1;
|
|
||||||
|
|
||||||
const tryUpdate = () => {
|
|
||||||
this._fetchData().catch(() => {
|
|
||||||
tries += 1;
|
|
||||||
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
tryUpdate();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,6 +4,8 @@ import {
|
|||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
|
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
|
||||||
import {
|
import {
|
||||||
@@ -33,6 +35,9 @@ class HassioPanel extends LitElement {
|
|||||||
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
|
if (!this.supervisorInfo) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
return html`
|
return html`
|
||||||
<hassio-panel-router
|
<hassio-panel-router
|
||||||
.route=${this.route}
|
.route=${this.route}
|
||||||
@@ -46,6 +51,16 @@ class HassioPanel extends LitElement {
|
|||||||
></hassio-panel-router>
|
></hassio-panel-router>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
--app-header-background-color: var(--sidebar-background-color);
|
||||||
|
--app-header-text-color: var(--sidebar-text-color);
|
||||||
|
--app-header-border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
150
hassio/src/hassio-router.ts
Normal file
150
hassio/src/hassio-router.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import {
|
||||||
|
customElement,
|
||||||
|
property,
|
||||||
|
internalProperty,
|
||||||
|
PropertyValues,
|
||||||
|
} from "lit-element";
|
||||||
|
import {
|
||||||
|
fetchHassioHassOsInfo,
|
||||||
|
fetchHassioHostInfo,
|
||||||
|
HassioHassOSInfo,
|
||||||
|
HassioHostInfo,
|
||||||
|
} from "../../src/data/hassio/host";
|
||||||
|
import {
|
||||||
|
fetchHassioHomeAssistantInfo,
|
||||||
|
fetchHassioSupervisorInfo,
|
||||||
|
fetchHassioInfo,
|
||||||
|
HassioHomeAssistantInfo,
|
||||||
|
HassioInfo,
|
||||||
|
HassioPanelInfo,
|
||||||
|
HassioSupervisorInfo,
|
||||||
|
} from "../../src/data/hassio/supervisor";
|
||||||
|
import {
|
||||||
|
HassRouterPage,
|
||||||
|
RouterOptions,
|
||||||
|
} from "../../src/layouts/hass-router-page";
|
||||||
|
import "../../src/resources/ha-style";
|
||||||
|
import { HomeAssistant } from "../../src/types";
|
||||||
|
// Don't codesplit it, that way the dashboard always loads fast.
|
||||||
|
import "./hassio-panel";
|
||||||
|
|
||||||
|
@customElement("hassio-router")
|
||||||
|
class HassioRouter extends HassRouterPage {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public panel!: HassioPanelInfo;
|
||||||
|
|
||||||
|
@property() public narrow!: boolean;
|
||||||
|
|
||||||
|
protected routerOptions: RouterOptions = {
|
||||||
|
// Hass.io has a page with tabs, so we route all non-matching routes to it.
|
||||||
|
defaultPage: "dashboard",
|
||||||
|
initialLoad: () => this._fetchData(),
|
||||||
|
showLoading: true,
|
||||||
|
routes: {
|
||||||
|
dashboard: {
|
||||||
|
tag: "hassio-panel",
|
||||||
|
cache: true,
|
||||||
|
},
|
||||||
|
snapshots: "dashboard",
|
||||||
|
store: "dashboard",
|
||||||
|
system: "dashboard",
|
||||||
|
addon: {
|
||||||
|
tag: "hassio-addon-dashboard",
|
||||||
|
load: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
ingress: {
|
||||||
|
tag: "hassio-ingress-view",
|
||||||
|
load: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
@internalProperty() private _supervisorInfo: HassioSupervisorInfo;
|
||||||
|
|
||||||
|
@internalProperty() private _hostInfo: HassioHostInfo;
|
||||||
|
|
||||||
|
@internalProperty() private _hassioInfo?: HassioInfo;
|
||||||
|
|
||||||
|
@internalProperty() private _hassOsInfo?: HassioHassOSInfo;
|
||||||
|
|
||||||
|
@internalProperty() private _hassInfo: HassioHomeAssistantInfo;
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updatePageEl(el) {
|
||||||
|
// the tabs page does its own routing so needs full route.
|
||||||
|
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
|
||||||
|
|
||||||
|
el.hass = this.hass;
|
||||||
|
el.narrow = this.narrow;
|
||||||
|
el.supervisorInfo = this._supervisorInfo;
|
||||||
|
el.hassioInfo = this._hassioInfo;
|
||||||
|
el.hostInfo = this._hostInfo;
|
||||||
|
el.hassInfo = this._hassInfo;
|
||||||
|
el.hassOsInfo = this._hassOsInfo;
|
||||||
|
el.route = route;
|
||||||
|
|
||||||
|
if (el.localName === "hassio-ingress-view") {
|
||||||
|
el.ingressPanel = this.panel.config && this.panel.config.ingress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchData() {
|
||||||
|
if (this.panel.config && this.panel.config.ingress) {
|
||||||
|
this._redirectIngress(this.panel.config.ingress);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [supervisorInfo, hostInfo, hassInfo, hassioInfo] = await Promise.all([
|
||||||
|
fetchHassioSupervisorInfo(this.hass),
|
||||||
|
fetchHassioHostInfo(this.hass),
|
||||||
|
fetchHassioHomeAssistantInfo(this.hass),
|
||||||
|
fetchHassioInfo(this.hass),
|
||||||
|
]);
|
||||||
|
this._supervisorInfo = supervisorInfo;
|
||||||
|
this._hassioInfo = hassioInfo;
|
||||||
|
this._hostInfo = hostInfo;
|
||||||
|
this._hassInfo = hassInfo;
|
||||||
|
|
||||||
|
if (this._hostInfo.features && this._hostInfo.features.includes("hassos")) {
|
||||||
|
this._hassOsInfo = await fetchHassioHassOsInfo(this.hass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _redirectIngress(addonSlug: string) {
|
||||||
|
this.route = { prefix: "/hassio", path: `/ingress/${addonSlug}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
private _apiCalled(ev) {
|
||||||
|
if (!ev.detail.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tries = 1;
|
||||||
|
|
||||||
|
const tryUpdate = () => {
|
||||||
|
this._fetchData().catch(() => {
|
||||||
|
tries += 1;
|
||||||
|
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
tryUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hassio-router": HassioRouter;
|
||||||
|
}
|
||||||
|
}
|
@@ -17,6 +17,10 @@ import { createHassioSession } from "../../../src/data/hassio/supervisor";
|
|||||||
import "../../../src/layouts/hass-loading-screen";
|
import "../../../src/layouts/hass-loading-screen";
|
||||||
import "../../../src/layouts/hass-subpage";
|
import "../../../src/layouts/hass-subpage";
|
||||||
import { HomeAssistant, Route } from "../../../src/types";
|
import { HomeAssistant, Route } from "../../../src/types";
|
||||||
|
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||||
|
import { navigate } from "../../../src/common/navigate";
|
||||||
|
import { mdiMenu } from "@mdi/js";
|
||||||
|
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||||
|
|
||||||
@customElement("hassio-ingress-view")
|
@customElement("hassio-ingress-view")
|
||||||
class HassioIngressView extends LitElement {
|
class HassioIngressView extends LitElement {
|
||||||
@@ -24,22 +28,45 @@ class HassioIngressView extends LitElement {
|
|||||||
|
|
||||||
@property() public route!: Route;
|
@property() public route!: Route;
|
||||||
|
|
||||||
|
@property() public ingressPanel = false;
|
||||||
|
|
||||||
@internalProperty() private _addon?: HassioAddonDetails;
|
@internalProperty() private _addon?: HassioAddonDetails;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public narrow = false;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this._addon) {
|
if (!this._addon) {
|
||||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
const iframe = html`<iframe src=${this._addon.ingress_url!}></iframe>`;
|
||||||
<hass-subpage .header=${this._addon.name} hassio>
|
|
||||||
<iframe src=${this._addon.ingress_url}></iframe>
|
if (!this.ingressPanel) {
|
||||||
</hass-subpage>
|
return html`<hass-subpage
|
||||||
`;
|
.header=${this._addon.name}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
>
|
||||||
|
${iframe}
|
||||||
|
</hass-subpage>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`${this.narrow || this.hass.dockedSidebar === "always_hidden"
|
||||||
|
? html`<div class="header">
|
||||||
|
<mwc-icon-button
|
||||||
|
aria-label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
|
||||||
|
@click=${this._toggleMenu}
|
||||||
|
>
|
||||||
|
<ha-svg-icon path=${mdiMenu}></ha-svg-icon>
|
||||||
|
</mwc-icon-button>
|
||||||
|
<div class="main-title">${this._addon.name}</div>
|
||||||
|
</div>
|
||||||
|
${iframe}`
|
||||||
|
: iframe}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
protected updated(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.updated(changedProps);
|
||||||
|
|
||||||
if (!changedProps.has("route")) {
|
if (!changedProps.has("route")) {
|
||||||
return;
|
return;
|
||||||
@@ -56,27 +83,56 @@ class HassioIngressView extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchData(addonSlug: string) {
|
private async _fetchData(addonSlug: string) {
|
||||||
|
const createSessionPromise = createHassioSession(this.hass).then(
|
||||||
|
() => true,
|
||||||
|
() => false
|
||||||
|
);
|
||||||
|
|
||||||
|
let addon;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [addon] = await Promise.all([
|
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
|
||||||
fetchHassioAddonInfo(this.hass, addonSlug).catch(() => {
|
|
||||||
throw new Error("Failed to fetch add-on info");
|
|
||||||
}),
|
|
||||||
createHassioSession(this.hass).catch(() => {
|
|
||||||
throw new Error("Failed to create an ingress session");
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!addon.ingress) {
|
|
||||||
throw new Error("This add-on does not support ingress");
|
|
||||||
}
|
|
||||||
|
|
||||||
this._addon = addon;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// eslint-disable-next-line
|
await showAlertDialog(this, {
|
||||||
console.error(err);
|
text: "Unable to fetch add-on info to start Ingress",
|
||||||
alert(err.message || "Unknown error starting ingress.");
|
title: "Supervisor",
|
||||||
|
});
|
||||||
history.back();
|
history.back();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!addon.ingress_url) {
|
||||||
|
await showAlertDialog(this, {
|
||||||
|
text: "Add-on does not support Ingress",
|
||||||
|
title: addon.name,
|
||||||
|
});
|
||||||
|
history.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addon.state !== "started") {
|
||||||
|
await showAlertDialog(this, {
|
||||||
|
text: "Add-on is not running. Please start it first",
|
||||||
|
title: addon.name,
|
||||||
|
});
|
||||||
|
navigate(this, `/hassio/addon/${addon.slug}/info`, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await createSessionPromise)) {
|
||||||
|
await showAlertDialog(this, {
|
||||||
|
text: "Unable to create an Ingress session",
|
||||||
|
title: addon.name,
|
||||||
|
});
|
||||||
|
history.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._addon = addon;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleMenu(): void {
|
||||||
|
fireEvent(this, "hass-toggle-menu");
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult {
|
static get styles(): CSSResult {
|
||||||
@@ -87,6 +143,41 @@ class HassioIngressView extends LitElement {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header + iframe {
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 16px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 16px;
|
||||||
|
pointer-events: none;
|
||||||
|
background-color: var(--app-header-background-color);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--app-header-text-color, white);
|
||||||
|
border-bottom: var(--app-header-border-bottom, none);
|
||||||
|
box-sizing: border-box;
|
||||||
|
--mdc-icon-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
margin: 0 0 0 24px;
|
||||||
|
line-height: 20px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
mwc-icon-button {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
hass-subpage {
|
||||||
|
--app-header-background-color: var(--sidebar-background-color);
|
||||||
|
--app-header-text-color: var(--sidebar-text-color);
|
||||||
|
--app-header-border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,15 +13,17 @@ import {
|
|||||||
CSSResultArray,
|
CSSResultArray,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||||
|
import "../../../src/components/buttons/ha-progress-button";
|
||||||
import "../../../src/components/ha-card";
|
import "../../../src/components/ha-card";
|
||||||
import "../../../src/components/ha-svg-icon";
|
import "../../../src/components/ha-svg-icon";
|
||||||
|
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||||
import {
|
import {
|
||||||
createHassioFullSnapshot,
|
createHassioFullSnapshot,
|
||||||
createHassioPartialSnapshot,
|
createHassioPartialSnapshot,
|
||||||
@@ -80,8 +82,6 @@ class HassioSnapshots extends LitElement {
|
|||||||
{ slug: "addons/local", name: "Local add-ons", checked: true },
|
{ slug: "addons/local", name: "Local add-ons", checked: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
@internalProperty() private _creatingSnapshot = false;
|
|
||||||
|
|
||||||
@internalProperty() private _error = "";
|
@internalProperty() private _error = "";
|
||||||
|
|
||||||
public async refreshData() {
|
public async refreshData() {
|
||||||
@@ -192,12 +192,9 @@ class HassioSnapshots extends LitElement {
|
|||||||
: undefined}
|
: undefined}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<mwc-button
|
<ha-progress-button @click=${this._createSnapshot}>
|
||||||
.disabled=${this._creatingSnapshot}
|
|
||||||
@click=${this._createSnapshot}
|
|
||||||
>
|
|
||||||
Create
|
Create
|
||||||
</mwc-button>
|
</ha-progress-button>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,7 +227,7 @@ class HassioSnapshots extends LitElement {
|
|||||||
.icon=${snapshot.type === "full"
|
.icon=${snapshot.type === "full"
|
||||||
? mdiPackageVariantClosed
|
? mdiPackageVariantClosed
|
||||||
: mdiPackageVariant}
|
: mdiPackageVariant}
|
||||||
.icon-class="snapshot"
|
icon-class="snapshot"
|
||||||
></hassio-card-content>
|
></hassio-card-content>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
@@ -293,17 +290,20 @@ class HassioSnapshots extends LitElement {
|
|||||||
this._snapshots = await fetchHassioSnapshots(this.hass);
|
this._snapshots = await fetchHassioSnapshots(this.hass);
|
||||||
this._snapshots.sort((a, b) => (a.date < b.date ? 1 : -1));
|
this._snapshots.sort((a, b) => (a.date < b.date ? 1 : -1));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = err.message;
|
this._error = extractApiErrorMessage(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _createSnapshot() {
|
private async _createSnapshot(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
this._error = "";
|
this._error = "";
|
||||||
if (this._snapshotHasPassword && !this._snapshotPassword.length) {
|
if (this._snapshotHasPassword && !this._snapshotPassword.length) {
|
||||||
this._error = "Please enter a password.";
|
this._error = "Please enter a password.";
|
||||||
|
button.progress = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._creatingSnapshot = true;
|
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
|
|
||||||
const name =
|
const name =
|
||||||
@@ -343,10 +343,9 @@ class HassioSnapshots extends LitElement {
|
|||||||
this._updateSnapshots();
|
this._updateSnapshots();
|
||||||
fireEvent(this, "hass-api-called", { success: true, response: null });
|
fireEvent(this, "hass-api-called", { success: true, response: null });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = err.message;
|
this._error = extractApiErrorMessage(err);
|
||||||
} finally {
|
|
||||||
this._creatingSnapshot = false;
|
|
||||||
}
|
}
|
||||||
|
button.progress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _computeDetails(snapshot: HassioSnapshot) {
|
private _computeDetails(snapshot: HassioSnapshot) {
|
||||||
|
@@ -1,18 +1,32 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
|
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||||
|
import "@material/mwc-list/mwc-list-item";
|
||||||
|
import { mdiDotsVertical } from "@mdi/js";
|
||||||
|
import { safeDump } from "js-yaml";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResult,
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import "../../../src/components/buttons/ha-call-api-button";
|
import memoizeOne from "memoize-one";
|
||||||
|
import { atLeastVersion } from "../../../src/common/config/version";
|
||||||
|
import "../../../src/components/buttons/ha-progress-button";
|
||||||
|
import "../../../src/components/ha-button-menu";
|
||||||
|
import "../../../src/components/ha-card";
|
||||||
|
import "../../../src/components/ha-settings-row";
|
||||||
|
import {
|
||||||
|
extractApiErrorMessage,
|
||||||
|
ignoredStatusCodes,
|
||||||
|
} from "../../../src/data/hassio/common";
|
||||||
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
|
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
|
||||||
import {
|
import {
|
||||||
changeHostOptions,
|
changeHostOptions,
|
||||||
|
configSyncOS,
|
||||||
fetchHassioHostInfo,
|
fetchHassioHostInfo,
|
||||||
HassioHassOSInfo,
|
HassioHassOSInfo,
|
||||||
HassioHostInfo as HassioHostInfoType,
|
HassioHostInfo as HassioHostInfoType,
|
||||||
@@ -20,6 +34,10 @@ import {
|
|||||||
shutdownHost,
|
shutdownHost,
|
||||||
updateOS,
|
updateOS,
|
||||||
} from "../../../src/data/hassio/host";
|
} from "../../../src/data/hassio/host";
|
||||||
|
import {
|
||||||
|
fetchNetworkInfo,
|
||||||
|
NetworkInfo,
|
||||||
|
} from "../../../src/data/hassio/network";
|
||||||
import { HassioInfo } from "../../../src/data/hassio/supervisor";
|
import { HassioInfo } from "../../../src/data/hassio/supervisor";
|
||||||
import {
|
import {
|
||||||
showAlertDialog,
|
showAlertDialog,
|
||||||
@@ -29,6 +47,7 @@ import {
|
|||||||
import { haStyle } from "../../../src/resources/styles";
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../src/types";
|
import { HomeAssistant } from "../../../src/types";
|
||||||
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
|
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
|
||||||
|
import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
|
||||||
import { hassioStyle } from "../resources/hassio-style";
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
|
|
||||||
@customElement("hassio-host-info")
|
@customElement("hassio-host-info")
|
||||||
@@ -41,164 +60,179 @@ class HassioHostInfo extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
||||||
|
|
||||||
@internalProperty() private _errors?: string;
|
@internalProperty() public _networkInfo?: NetworkInfo;
|
||||||
|
|
||||||
public render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
|
const primaryIpAddress = this.hostInfo.features.includes("network")
|
||||||
|
? this._primaryIpAddress(this._networkInfo!)
|
||||||
|
: "";
|
||||||
return html`
|
return html`
|
||||||
<ha-card>
|
<ha-card header="Host System">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<h2>Host system</h2>
|
|
||||||
<table class="info">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Hostname</td>
|
|
||||||
<td>${this.hostInfo.hostname}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>System</td>
|
|
||||||
<td>${this.hostInfo.operating_system}</td>
|
|
||||||
</tr>
|
|
||||||
${!this.hostInfo.features.includes("hassos")
|
|
||||||
? html`<tr>
|
|
||||||
<td>Docker version</td>
|
|
||||||
<td>${this.hassioInfo.docker}</td>
|
|
||||||
</tr>`
|
|
||||||
: ""}
|
|
||||||
${this.hostInfo.deployment
|
|
||||||
? html`
|
|
||||||
<tr>
|
|
||||||
<td>Deployment</td>
|
|
||||||
<td>${this.hostInfo.deployment}</td>
|
|
||||||
</tr>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<mwc-button raised @click=${this._showHardware} class="info">
|
|
||||||
Hardware
|
|
||||||
</mwc-button>
|
|
||||||
${this.hostInfo.features.includes("hostname")
|
${this.hostInfo.features.includes("hostname")
|
||||||
? html`
|
? html`<ha-settings-row>
|
||||||
|
<span slot="heading">
|
||||||
|
Hostname
|
||||||
|
</span>
|
||||||
|
<span slot="description">
|
||||||
|
${this.hostInfo.hostname}
|
||||||
|
</span>
|
||||||
<mwc-button
|
<mwc-button
|
||||||
raised
|
title="Change the hostname"
|
||||||
|
label="Change"
|
||||||
@click=${this._changeHostnameClicked}
|
@click=${this._changeHostnameClicked}
|
||||||
class="info"
|
|
||||||
>
|
>
|
||||||
Change hostname
|
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
`
|
</ha-settings-row>`
|
||||||
: ""}
|
: ""}
|
||||||
${this._errors
|
${this.hostInfo.features.includes("network") &&
|
||||||
? html` <div class="errors">Error: ${this._errors}</div> `
|
atLeastVersion(this.hass.config.version, 0, 115)
|
||||||
|
? html` <ha-settings-row>
|
||||||
|
<span slot="heading">
|
||||||
|
IP address
|
||||||
|
</span>
|
||||||
|
<span slot="description">
|
||||||
|
${primaryIpAddress}
|
||||||
|
</span>
|
||||||
|
<mwc-button
|
||||||
|
title="Change the network"
|
||||||
|
label="Change"
|
||||||
|
@click=${this._changeNetworkClicked}
|
||||||
|
>
|
||||||
|
</mwc-button>
|
||||||
|
</ha-settings-row>`
|
||||||
|
: ""}
|
||||||
|
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading">
|
||||||
|
Operating system
|
||||||
|
</span>
|
||||||
|
<span slot="description">
|
||||||
|
${this.hostInfo.operating_system}
|
||||||
|
</span>
|
||||||
|
${this.hostInfo.version !== this.hostInfo.version_latest &&
|
||||||
|
this.hostInfo.features.includes("hassos")
|
||||||
|
? html`
|
||||||
|
<ha-progress-button
|
||||||
|
title="Update the host OS"
|
||||||
|
@click=${this._osUpdate}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</ha-progress-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</ha-settings-row>
|
||||||
|
${!this.hostInfo.features.includes("hassos")
|
||||||
|
? html`<ha-settings-row>
|
||||||
|
<span slot="heading">
|
||||||
|
Docker version
|
||||||
|
</span>
|
||||||
|
<span slot="description">
|
||||||
|
${this.hassioInfo.docker}
|
||||||
|
</span>
|
||||||
|
</ha-settings-row>`
|
||||||
|
: ""}
|
||||||
|
${this.hostInfo.deployment
|
||||||
|
? html`<ha-settings-row>
|
||||||
|
<span slot="heading">
|
||||||
|
Deployment
|
||||||
|
</span>
|
||||||
|
<span slot="description">
|
||||||
|
${this.hostInfo.deployment}
|
||||||
|
</span>
|
||||||
|
</ha-settings-row>`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
${this.hostInfo.features.includes("reboot")
|
${this.hostInfo.features.includes("reboot")
|
||||||
? html`
|
? html`
|
||||||
<mwc-button class="warning" @click=${this._rebootHost}
|
<ha-progress-button
|
||||||
>Reboot</mwc-button
|
title="Reboot the host OS"
|
||||||
|
class="warning"
|
||||||
|
@click=${this._hostReboot}
|
||||||
>
|
>
|
||||||
|
Reboot
|
||||||
|
</ha-progress-button>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
${this.hostInfo.features.includes("shutdown")
|
${this.hostInfo.features.includes("shutdown")
|
||||||
? html`
|
? html`
|
||||||
<mwc-button class="warning" @click=${this._shutdownHost}
|
<ha-progress-button
|
||||||
>Shutdown</mwc-button
|
title="Shutdown the host OS"
|
||||||
>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${this.hostInfo.features.includes("hassos")
|
|
||||||
? html`
|
|
||||||
<ha-call-api-button
|
|
||||||
class="warning"
|
class="warning"
|
||||||
.hass=${this.hass}
|
@click=${this._hostShutdown}
|
||||||
path="hassio/os/config/sync"
|
|
||||||
title="Load HassOS configs or updates from USB"
|
|
||||||
>Import from USB</ha-call-api-button
|
|
||||||
>
|
>
|
||||||
|
Shutdown
|
||||||
|
</ha-progress-button>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
${this.hostInfo.version !== this.hostInfo.version_latest
|
|
||||||
? html` <mwc-button @click=${this._updateOS}>Update</mwc-button> `
|
<ha-button-menu
|
||||||
: ""}
|
corner="BOTTOM_START"
|
||||||
|
@action=${this._handleMenuAction}
|
||||||
|
>
|
||||||
|
<mwc-icon-button slot="trigger">
|
||||||
|
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
|
||||||
|
</mwc-icon-button>
|
||||||
|
<mwc-list-item title="Show a list of hardware">
|
||||||
|
Hardware
|
||||||
|
</mwc-list-item>
|
||||||
|
${this.hostInfo.features.includes("hassos")
|
||||||
|
? html`<mwc-list-item
|
||||||
|
title="Load HassOS configs or updates from USB"
|
||||||
|
>
|
||||||
|
Import from USB
|
||||||
|
</mwc-list-item>`
|
||||||
|
: ""}
|
||||||
|
</ha-button-menu>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
|
||||||
return [
|
|
||||||
haStyle,
|
|
||||||
hassioStyle,
|
|
||||||
css`
|
|
||||||
ha-card {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.card-content {
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: calc(100% - 47px);
|
|
||||||
}
|
|
||||||
.info {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.info td:nth-child(2) {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
.errors {
|
|
||||||
color: var(--error-color);
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
mwc-button.info {
|
|
||||||
max-width: calc(50% - 12px);
|
|
||||||
}
|
|
||||||
table.info {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.warning {
|
|
||||||
--mdc-theme-primary: var(--error-color);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected firstUpdated(): void {
|
protected firstUpdated(): void {
|
||||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
this._loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _apiCalled(ev): void {
|
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
|
||||||
if (ev.detail.success) {
|
if (!network_info) {
|
||||||
this._errors = undefined;
|
return "";
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return Object.keys(network_info?.interfaces)
|
||||||
|
.map((device) => network_info.interfaces[device])
|
||||||
|
.find((device) => device.primary)?.ip_address;
|
||||||
|
});
|
||||||
|
|
||||||
const response = ev.detail.response;
|
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||||
|
switch (ev.detail.index) {
|
||||||
this._errors =
|
case 0:
|
||||||
typeof response.body === "object"
|
await this._showHardware();
|
||||||
? response.body.message || "Unknown error"
|
break;
|
||||||
: response.body;
|
case 1:
|
||||||
|
await this._importFromUSB();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _showHardware(): Promise<void> {
|
private async _showHardware(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const content = this._objectToMarkdown(
|
const content = await fetchHassioHardwareInfo(this.hass);
|
||||||
await fetchHassioHardwareInfo(this.hass)
|
|
||||||
);
|
|
||||||
showHassioMarkdownDialog(this, {
|
showHassioMarkdownDialog(this, {
|
||||||
title: "Hardware",
|
title: "Hardware",
|
||||||
content,
|
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showHassioMarkdownDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: "Hardware",
|
title: "Failed to get Hardware list",
|
||||||
content: "Error getting hardware info",
|
text: extractApiErrorMessage(err),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _rebootHost(): Promise<void> {
|
private async _hostReboot(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
const confirmed = await showConfirmationDialog(this, {
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
title: "Reboot",
|
title: "Reboot",
|
||||||
text: "Are you sure you want to reboot the host?",
|
text: "Are you sure you want to reboot the host?",
|
||||||
@@ -207,20 +241,28 @@ class HassioHostInfo extends LitElement {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
|
button.progress = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await rebootHost(this.hass);
|
await rebootHost(this.hass);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlertDialog(this, {
|
// Ignore connection errors, these are all expected
|
||||||
title: "Failed to reboot",
|
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
|
||||||
text: err.body.message,
|
showAlertDialog(this, {
|
||||||
});
|
title: "Failed to reboot",
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
button.progress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _shutdownHost(): Promise<void> {
|
private async _hostShutdown(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
const confirmed = await showConfirmationDialog(this, {
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
title: "Shutdown",
|
title: "Shutdown",
|
||||||
text: "Are you sure you want to shutdown the host?",
|
text: "Are you sure you want to shutdown the host?",
|
||||||
@@ -229,20 +271,28 @@ class HassioHostInfo extends LitElement {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
|
button.progress = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await shutdownHost(this.hass);
|
await shutdownHost(this.hass);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlertDialog(this, {
|
// Ignore connection errors, these are all expected
|
||||||
title: "Failed to shutdown",
|
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
|
||||||
text: err.body.message,
|
showAlertDialog(this, {
|
||||||
});
|
title: "Failed to shutdown",
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
button.progress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _updateOS(): Promise<void> {
|
private async _osUpdate(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
const confirmed = await showConfirmationDialog(this, {
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
title: "Update",
|
title: "Update",
|
||||||
text: "Are you sure you want to update the OS?",
|
text: "Are you sure you want to update the OS?",
|
||||||
@@ -251,6 +301,7 @@ class HassioHostInfo extends LitElement {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
|
button.progress = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,30 +310,17 @@ class HassioHostInfo extends LitElement {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: "Failed to update",
|
title: "Failed to update",
|
||||||
text: err.body.message,
|
text: extractApiErrorMessage(err),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
button.progress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _objectToMarkdown(obj, indent = ""): string {
|
private async _changeNetworkClicked(): Promise<void> {
|
||||||
let data = "";
|
showNetworkDialog(this, {
|
||||||
Object.keys(obj).forEach((key) => {
|
network: this._networkInfo!,
|
||||||
if (typeof obj[key] !== "object") {
|
loadData: () => this._loadData(),
|
||||||
data += `${indent}- ${key}: ${obj[key]}\n`;
|
|
||||||
} else {
|
|
||||||
data += `${indent}- ${key}:\n`;
|
|
||||||
if (Array.isArray(obj[key])) {
|
|
||||||
if (obj[key].length) {
|
|
||||||
data +=
|
|
||||||
`${indent} - ` + obj[key].join(`\n${indent} - `) + "\n";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
data += this._objectToMarkdown(obj[key], ` ${indent}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _changeHostnameClicked(): Promise<void> {
|
private async _changeHostnameClicked(): Promise<void> {
|
||||||
@@ -301,11 +339,83 @@ class HassioHostInfo extends LitElement {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: "Setting hostname failed",
|
title: "Setting hostname failed",
|
||||||
text: err.body.message,
|
text: extractApiErrorMessage(err),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _importFromUSB(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await configSyncOS(this.hass);
|
||||||
|
this.hostInfo = await fetchHassioHostInfo(this.hass);
|
||||||
|
} catch (err) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: "Failed to import from USB",
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _loadData(): Promise<void> {
|
||||||
|
this._networkInfo = await fetchNetworkInfo(this.hass);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
hassioStyle,
|
||||||
|
css`
|
||||||
|
ha-card {
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
height: 48px;
|
||||||
|
border-top: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
ha-settings-row {
|
||||||
|
padding: 0;
|
||||||
|
height: 54px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
ha-settings-row[three-line] {
|
||||||
|
height: 74px;
|
||||||
|
}
|
||||||
|
ha-settings-row > span[slot="description"] {
|
||||||
|
white-space: normal;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
--mdc-theme-primary: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-button-menu {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
--mdc-menu-min-width: 200px;
|
||||||
|
}
|
||||||
|
@media (min-width: 563px) {
|
||||||
|
paper-listbox {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paper-item {
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 35px;
|
||||||
|
}
|
||||||
|
mwc-list-item ha-svg-icon {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import "@material/mwc-button";
|
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResult,
|
CSSResult,
|
||||||
@@ -6,21 +5,29 @@ import {
|
|||||||
html,
|
html,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
import "../../../src/components/buttons/ha-progress-button";
|
||||||
import "../../../src/components/buttons/ha-call-api-button";
|
|
||||||
import "../../../src/components/ha-card";
|
import "../../../src/components/ha-card";
|
||||||
|
import "../../../src/components/ha-settings-row";
|
||||||
|
import "../../../src/components/ha-switch";
|
||||||
|
import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host";
|
||||||
import {
|
import {
|
||||||
HassioSupervisorInfo as HassioSupervisorInfoType,
|
HassioSupervisorInfo as HassioSupervisorInfoType,
|
||||||
|
reloadSupervisor,
|
||||||
setSupervisorOption,
|
setSupervisorOption,
|
||||||
SupervisorOptions,
|
SupervisorOptions,
|
||||||
|
updateSupervisor,
|
||||||
|
fetchHassioSupervisorInfo,
|
||||||
} from "../../../src/data/hassio/supervisor";
|
} from "../../../src/data/hassio/supervisor";
|
||||||
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
import {
|
||||||
|
showAlertDialog,
|
||||||
|
showConfirmationDialog,
|
||||||
|
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||||
import { haStyle } from "../../../src/resources/styles";
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../src/types";
|
import { HomeAssistant } from "../../../src/types";
|
||||||
import { hassioStyle } from "../resources/hassio-style";
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
|
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||||
|
|
||||||
@customElement("hassio-supervisor-info")
|
@customElement("hassio-supervisor-info")
|
||||||
class HassioSupervisorInfo extends LitElement {
|
class HassioSupervisorInfo extends LitElement {
|
||||||
@@ -28,75 +35,234 @@ class HassioSupervisorInfo extends LitElement {
|
|||||||
|
|
||||||
@property() public supervisorInfo!: HassioSupervisorInfoType;
|
@property() public supervisorInfo!: HassioSupervisorInfoType;
|
||||||
|
|
||||||
@internalProperty() private _errors?: string;
|
@property() public hostInfo!: HassioHostInfoType;
|
||||||
|
|
||||||
public render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
return html`
|
return html`
|
||||||
<ha-card>
|
<ha-card header="Supervisor">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<h2>Supervisor</h2>
|
<ha-settings-row>
|
||||||
<table class="info">
|
<span slot="heading">
|
||||||
<tbody>
|
Version
|
||||||
<tr>
|
</span>
|
||||||
<td>Version</td>
|
<span slot="description">
|
||||||
<td>${this.supervisorInfo.version}</td>
|
${this.supervisorInfo.version}
|
||||||
</tr>
|
</span>
|
||||||
<tr>
|
</ha-settings-row>
|
||||||
<td>Latest version</td>
|
<ha-settings-row>
|
||||||
<td>${this.supervisorInfo.version_latest}</td>
|
<span slot="heading">
|
||||||
</tr>
|
Newest version
|
||||||
${this.supervisorInfo.channel !== "stable"
|
</span>
|
||||||
? html`
|
<span slot="description">
|
||||||
<tr>
|
${this.supervisorInfo.version_latest}
|
||||||
<td>Channel</td>
|
</span>
|
||||||
<td>${this.supervisorInfo.channel}</td>
|
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
|
||||||
</tr>
|
? html`
|
||||||
`
|
<ha-progress-button
|
||||||
: ""}
|
title="Update the supervisor"
|
||||||
</tbody>
|
@click=${this._supervisorUpdate}
|
||||||
</table>
|
>
|
||||||
${this._errors
|
Update
|
||||||
? html` <div class="errors">Error: ${this._errors}</div> `
|
</ha-progress-button>
|
||||||
: ""}
|
`
|
||||||
|
: ""}
|
||||||
|
</ha-settings-row>
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading">
|
||||||
|
Channel
|
||||||
|
</span>
|
||||||
|
<span slot="description">
|
||||||
|
${this.supervisorInfo.channel}
|
||||||
|
</span>
|
||||||
|
${this.supervisorInfo.channel === "beta"
|
||||||
|
? html`
|
||||||
|
<ha-progress-button
|
||||||
|
@click=${this._toggleBeta}
|
||||||
|
title="Get stable updates for Home Assistant, supervisor and host"
|
||||||
|
>
|
||||||
|
Leave beta channel
|
||||||
|
</ha-progress-button>
|
||||||
|
`
|
||||||
|
: this.supervisorInfo.channel === "stable"
|
||||||
|
? html`
|
||||||
|
<ha-progress-button
|
||||||
|
@click=${this._toggleBeta}
|
||||||
|
title="Get beta updates for Home Assistant (RCs), supervisor and host"
|
||||||
|
>
|
||||||
|
Join beta channel
|
||||||
|
</ha-progress-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</ha-settings-row>
|
||||||
|
|
||||||
|
${this.supervisorInfo?.supported
|
||||||
|
? html` <ha-settings-row three-line>
|
||||||
|
<span slot="heading">
|
||||||
|
Share diagnostics
|
||||||
|
</span>
|
||||||
|
<div slot="description" class="diagnostics-description">
|
||||||
|
Share crash reports and diagnostic information.
|
||||||
|
<button
|
||||||
|
class="link"
|
||||||
|
title="Show more information about this"
|
||||||
|
@click=${this._diagnosticsInformationDialog}
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ha-switch
|
||||||
|
haptic
|
||||||
|
.checked=${this.supervisorInfo.diagnostics}
|
||||||
|
@change=${this._toggleDiagnostics}
|
||||||
|
></ha-switch>
|
||||||
|
</ha-settings-row>`
|
||||||
|
: html`<div class="error">
|
||||||
|
You are running an unsupported installation.
|
||||||
|
<a
|
||||||
|
href="https://github.com/home-assistant/architecture/blob/master/adr/${this.hostInfo.features.includes(
|
||||||
|
"hassos"
|
||||||
|
)
|
||||||
|
? "0015-home-assistant-os.md"
|
||||||
|
: "0014-home-assistant-supervised.md"}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
title="Learn more about how you can make your system compliant"
|
||||||
|
>
|
||||||
|
Learn More
|
||||||
|
</a>
|
||||||
|
</div>`}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<ha-call-api-button .hass=${this.hass} path="hassio/supervisor/reload"
|
<ha-progress-button
|
||||||
>Reload</ha-call-api-button
|
@click=${this._supervisorReload}
|
||||||
|
title="Reload parts of the supervisor."
|
||||||
>
|
>
|
||||||
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
|
Reload
|
||||||
? html`
|
</ha-progress-button>
|
||||||
<ha-call-api-button
|
|
||||||
.hass=${this.hass}
|
|
||||||
path="hassio/supervisor/update"
|
|
||||||
>Update</ha-call-api-button
|
|
||||||
>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${this.supervisorInfo.channel === "beta"
|
|
||||||
? html`
|
|
||||||
<ha-call-api-button
|
|
||||||
.hass=${this.hass}
|
|
||||||
path="hassio/supervisor/options"
|
|
||||||
.data=${{ channel: "stable" }}
|
|
||||||
>Leave beta channel</ha-call-api-button
|
|
||||||
>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${this.supervisorInfo.channel === "stable"
|
|
||||||
? html`
|
|
||||||
<mwc-button
|
|
||||||
@click=${this._joinBeta}
|
|
||||||
class="warning"
|
|
||||||
title="Get beta updates for Home Assistant (RCs), supervisor and host"
|
|
||||||
>Join beta channel</mwc-button
|
|
||||||
>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _toggleBeta(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
|
if (this.supervisorInfo.channel === "stable") {
|
||||||
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
|
title: "WARNING",
|
||||||
|
text: html` Beta releases are for testers and early adopters and can
|
||||||
|
contain unstable code changes.
|
||||||
|
<br />
|
||||||
|
<b>
|
||||||
|
Make sure you have backups of your data before you activate this
|
||||||
|
feature.
|
||||||
|
</b>
|
||||||
|
<br /><br />
|
||||||
|
This includes beta releases for:
|
||||||
|
<li>Home Assistant Core</li>
|
||||||
|
<li>Home Assistant Supervisor</li>
|
||||||
|
<li>Home Assistant Operating System</li>
|
||||||
|
<br />
|
||||||
|
Do you want to join the beta channel?`,
|
||||||
|
confirmText: "join beta",
|
||||||
|
dismissText: "no",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
button.progress = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data: Partial<SupervisorOptions> = {
|
||||||
|
channel: this.supervisorInfo.channel === "stable" ? "beta" : "stable",
|
||||||
|
};
|
||||||
|
await setSupervisorOption(this.hass, data);
|
||||||
|
await reloadSupervisor(this.hass);
|
||||||
|
this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
|
||||||
|
} catch (err) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: "Failed to set supervisor option",
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
button.progress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _supervisorReload(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await reloadSupervisor(this.hass);
|
||||||
|
this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
|
||||||
|
} catch (err) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: "Failed to reload the supervisor",
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
button.progress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _supervisorUpdate(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
|
title: "Update supervisor",
|
||||||
|
text: `Are you sure you want to upgrade supervisor to version ${this.supervisorInfo.version_latest}?`,
|
||||||
|
confirmText: "update",
|
||||||
|
dismissText: "cancel",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
button.progress = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateSupervisor(this.hass);
|
||||||
|
} catch (err) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: "Failed to update the supervisor",
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
button.progress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _diagnosticsInformationDialog(): Promise<void> {
|
||||||
|
await showAlertDialog(this, {
|
||||||
|
title: "Help Improve Home Assistant",
|
||||||
|
text: html`Would you want to automatically share crash reports and
|
||||||
|
diagnostic information when the supervisor encounters unexpected errors?
|
||||||
|
<br /><br />
|
||||||
|
This will allow us to fix the problems, the information is only
|
||||||
|
accessible to the Home Assistant Core team and will not be shared with
|
||||||
|
others.
|
||||||
|
<br /><br />
|
||||||
|
The data does not include any private/sensitive information and you can
|
||||||
|
disable this in settings at any time you want.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _toggleDiagnostics(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const data: SupervisorOptions = {
|
||||||
|
diagnostics: !this.supervisorInfo?.diagnostics,
|
||||||
|
};
|
||||||
|
await setSupervisorOption(this.hass, data);
|
||||||
|
} catch (err) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: "Failed to set supervisor option",
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
@@ -104,83 +270,35 @@ class HassioSupervisorInfo extends LitElement {
|
|||||||
css`
|
css`
|
||||||
ha-card {
|
ha-card {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
height: 48px;
|
||||||
|
border-top: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
button.link {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
ha-settings-row {
|
||||||
|
padding: 0;
|
||||||
|
height: 54px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.card-content {
|
ha-settings-row[three-line] {
|
||||||
color: var(--primary-text-color);
|
height: 74px;
|
||||||
box-sizing: border-box;
|
|
||||||
height: calc(100% - 47px);
|
|
||||||
}
|
}
|
||||||
.info {
|
ha-settings-row > div[slot="description"] {
|
||||||
width: 100%;
|
white-space: normal;
|
||||||
}
|
color: var(--secondary-text-color);
|
||||||
.info td:nth-child(2) {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
.errors {
|
|
||||||
color: var(--error-color);
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(): void {
|
|
||||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
|
||||||
}
|
|
||||||
|
|
||||||
private _apiCalled(ev): void {
|
|
||||||
if (ev.detail.success) {
|
|
||||||
this._errors = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = ev.detail.response;
|
|
||||||
|
|
||||||
this._errors =
|
|
||||||
typeof response.body === "object"
|
|
||||||
? response.body.message || "Unknown error"
|
|
||||||
: response.body;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _joinBeta() {
|
|
||||||
const confirmed = await showConfirmationDialog(this, {
|
|
||||||
title: "WARNING",
|
|
||||||
text: html` Beta releases are for testers and early adopters and can
|
|
||||||
contain unstable code changes.
|
|
||||||
<br />
|
|
||||||
<b>
|
|
||||||
Make sure you have backups of your data before you activate this
|
|
||||||
feature.
|
|
||||||
</b>
|
|
||||||
<br /><br />
|
|
||||||
This includes beta releases for:
|
|
||||||
<li>Home Assistant Core</li>
|
|
||||||
<li>Home Assistant Supervisor</li>
|
|
||||||
<li>Home Assistant Operating System</li>
|
|
||||||
<br />
|
|
||||||
Do you want to join the beta channel?`,
|
|
||||||
confirmText: "join beta",
|
|
||||||
dismissText: "no",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data: SupervisorOptions = { channel: "beta" };
|
|
||||||
await setSupervisorOption(this.hass, data);
|
|
||||||
const eventdata = {
|
|
||||||
success: true,
|
|
||||||
response: undefined,
|
|
||||||
path: "option",
|
|
||||||
};
|
|
||||||
fireEvent(this, "hass-api-called", eventdata);
|
|
||||||
} catch (err) {
|
|
||||||
this._errors = `Error joining beta channel, ${err.body?.message || err}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -7,12 +7,14 @@ import {
|
|||||||
CSSResult,
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
|
import "../../../src/components/buttons/ha-progress-button";
|
||||||
import "../../../src/components/ha-card";
|
import "../../../src/components/ha-card";
|
||||||
|
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||||
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
|
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
|
||||||
import "../../../src/layouts/hass-loading-screen";
|
import "../../../src/layouts/hass-loading-screen";
|
||||||
import { haStyle } from "../../../src/resources/styles";
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
@@ -67,7 +69,7 @@ class HassioSupervisorLog extends LitElement {
|
|||||||
await this._loadData();
|
await this._loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
return html`
|
return html`
|
||||||
<ha-card>
|
<ha-card>
|
||||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
||||||
@@ -102,18 +104,49 @@ class HassioSupervisorLog extends LitElement {
|
|||||||
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
|
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<mwc-button @click=${this._refresh}>Refresh</mwc-button>
|
<ha-progress-button @click=${this._refresh}>
|
||||||
|
Refresh
|
||||||
|
</ha-progress-button>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _setLogProvider(ev): Promise<void> {
|
||||||
|
const provider = ev.detail.item.getAttribute("provider");
|
||||||
|
this._selectedLogProvider = provider;
|
||||||
|
this._loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _refresh(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
await this._loadData();
|
||||||
|
button.progress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _loadData(): Promise<void> {
|
||||||
|
this._error = undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this._content = await fetchHassioLogs(
|
||||||
|
this.hass,
|
||||||
|
this._selectedLogProvider
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to get supervisor logs, ${extractApiErrorMessage(
|
||||||
|
err
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
hassioStyle,
|
hassioStyle,
|
||||||
css`
|
css`
|
||||||
ha-card {
|
ha-card {
|
||||||
|
margin-top: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
pre {
|
pre {
|
||||||
@@ -127,38 +160,9 @@ class HassioSupervisorLog extends LitElement {
|
|||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
.card-content {
|
|
||||||
padding-top: 0px;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _setLogProvider(ev): Promise<void> {
|
|
||||||
const provider = ev.detail.item.getAttribute("provider");
|
|
||||||
this._selectedLogProvider = provider;
|
|
||||||
await this._loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _loadData(): Promise<void> {
|
|
||||||
this._error = undefined;
|
|
||||||
this._content = undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
this._content = await fetchHassioLogs(
|
|
||||||
this.hass,
|
|
||||||
this._selectedLogProvider
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
this._error = `Failed to get supervisor logs, ${
|
|
||||||
err.body?.message || err
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _refresh(): Promise<void> {
|
|
||||||
await this._loadData();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -12,8 +12,8 @@ import {
|
|||||||
HassioHostInfo,
|
HassioHostInfo,
|
||||||
} from "../../../src/data/hassio/host";
|
} from "../../../src/data/hassio/host";
|
||||||
import {
|
import {
|
||||||
HassioSupervisorInfo,
|
|
||||||
HassioInfo,
|
HassioInfo,
|
||||||
|
HassioSupervisorInfo,
|
||||||
} from "../../../src/data/hassio/supervisor";
|
} from "../../../src/data/hassio/supervisor";
|
||||||
import "../../../src/layouts/hass-tabs-subpage";
|
import "../../../src/layouts/hass-tabs-subpage";
|
||||||
import { haStyle } from "../../../src/resources/styles";
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
@@ -40,7 +40,7 @@ class HassioSystem extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
||||||
|
|
||||||
public render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
return html`
|
return html`
|
||||||
<hass-tabs-subpage
|
<hass-tabs-subpage
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -52,10 +52,10 @@ class HassioSystem extends LitElement {
|
|||||||
>
|
>
|
||||||
<span slot="header">System</span>
|
<span slot="header">System</span>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h1>Information</h1>
|
|
||||||
<div class="card-group">
|
<div class="card-group">
|
||||||
<hassio-supervisor-info
|
<hassio-supervisor-info
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
.hostInfo=${this.hostInfo}
|
||||||
.supervisorInfo=${this.supervisorInfo}
|
.supervisorInfo=${this.supervisorInfo}
|
||||||
></hassio-supervisor-info>
|
></hassio-supervisor-info>
|
||||||
<hassio-host-info
|
<hassio-host-info
|
||||||
@@ -65,7 +65,6 @@ class HassioSystem extends LitElement {
|
|||||||
.hassOsInfo=${this.hassOsInfo}
|
.hassOsInfo=${this.hassOsInfo}
|
||||||
></hassio-host-info>
|
></hassio-host-info>
|
||||||
</div>
|
</div>
|
||||||
<h1>System log</h1>
|
|
||||||
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
|
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
|
||||||
</div>
|
</div>
|
||||||
</hass-tabs-subpage>
|
</hass-tabs-subpage>
|
||||||
|
73
package.json
73
package.json
@@ -13,7 +13,7 @@
|
|||||||
"lint:prettier": "prettier '**/src/**/*.{js,ts,json,css,md}' --check",
|
"lint:prettier": "prettier '**/src/**/*.{js,ts,json,css,md}' --check",
|
||||||
"format:prettier": "prettier '**/src/**/*.{js,ts,json,css,md}' --write",
|
"format:prettier": "prettier '**/src/**/*.{js,ts,json,css,md}' --write",
|
||||||
"lint:types": "tsc",
|
"lint:types": "tsc",
|
||||||
"lint:lit": "lit-analyzer '**/src/**/*.ts'",
|
"lint:lit": "lit-analyzer '**/src/**/*.ts' --format markdown --outFile result.md",
|
||||||
"lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:types",
|
"lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:types",
|
||||||
"format": "npm run format:eslint && npm run format:prettier",
|
"format": "npm run format:eslint && npm run format:prettier",
|
||||||
"mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts",
|
"mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts",
|
||||||
@@ -23,25 +23,29 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/intl-pluralrules": "^1.5.8",
|
"@formatjs/intl-pluralrules": "^1.5.8",
|
||||||
"@fullcalendar/core": "^5.0.0-beta.2",
|
"@fullcalendar/common": "5.1.0",
|
||||||
"@fullcalendar/daygrid": "^5.0.0-beta.2",
|
"@fullcalendar/core": "5.1.0",
|
||||||
"@material/chips": "=8.0.0-canary.a78ceb112.0",
|
"@fullcalendar/daygrid": "5.1.0",
|
||||||
|
"@fullcalendar/interaction": "5.1.0",
|
||||||
|
"@fullcalendar/list": "5.1.0",
|
||||||
|
"@material/chips": "=8.0.0-canary.096a7a066.0",
|
||||||
"@material/circular-progress": "=8.0.0-canary.a78ceb112.0",
|
"@material/circular-progress": "=8.0.0-canary.a78ceb112.0",
|
||||||
"@material/mwc-button": "^0.17.2",
|
"@material/mwc-button": "^0.18.0",
|
||||||
"@material/mwc-checkbox": "^0.17.2",
|
"@material/mwc-checkbox": "^0.18.0",
|
||||||
"@material/mwc-dialog": "^0.17.2",
|
"@material/mwc-dialog": "^0.18.0",
|
||||||
"@material/mwc-fab": "^0.17.2",
|
"@material/mwc-fab": "^0.18.0",
|
||||||
"@material/mwc-formfield": "^0.17.2",
|
"@material/mwc-formfield": "^0.18.0",
|
||||||
"@material/mwc-icon-button": "^0.17.2",
|
"@material/mwc-icon-button": "^0.18.0",
|
||||||
"@material/mwc-list": "^0.17.2",
|
"@material/mwc-list": "^0.18.0",
|
||||||
"@material/mwc-menu": "^0.17.2",
|
"@material/mwc-menu": "^0.18.0",
|
||||||
"@material/mwc-ripple": "^0.17.2",
|
"@material/mwc-radio": "^0.18.0",
|
||||||
"@material/mwc-switch": "^0.17.2",
|
"@material/mwc-ripple": "^0.18.0",
|
||||||
"@material/mwc-tab": "^0.17.2",
|
"@material/mwc-switch": "^0.18.0",
|
||||||
"@material/mwc-tab-bar": "^0.17.2",
|
"@material/mwc-tab": "^0.18.0",
|
||||||
"@material/top-app-bar": "=8.0.0-canary.a78ceb112.0",
|
"@material/mwc-tab-bar": "^0.18.0",
|
||||||
"@mdi/js": "5.3.45",
|
"@material/top-app-bar": "=8.0.0-canary.096a7a066.0",
|
||||||
"@mdi/svg": "5.3.45",
|
"@mdi/js": "5.5.55",
|
||||||
|
"@mdi/svg": "5.5.55",
|
||||||
"@polymer/app-layout": "^3.0.2",
|
"@polymer/app-layout": "^3.0.2",
|
||||||
"@polymer/app-route": "^3.0.2",
|
"@polymer/app-route": "^3.0.2",
|
||||||
"@polymer/app-storage": "^3.0.2",
|
"@polymer/app-storage": "^3.0.2",
|
||||||
@@ -74,6 +78,8 @@
|
|||||||
"@polymer/paper-tooltip": "^3.0.1",
|
"@polymer/paper-tooltip": "^3.0.1",
|
||||||
"@polymer/polymer": "3.1.0",
|
"@polymer/polymer": "3.1.0",
|
||||||
"@thomasloven/round-slider": "0.5.0",
|
"@thomasloven/round-slider": "0.5.0",
|
||||||
|
"@types/chromecast-caf-sender": "^1.0.3",
|
||||||
|
"@types/sortablejs": "^1.10.6",
|
||||||
"@vaadin/vaadin-combo-box": "^5.0.10",
|
"@vaadin/vaadin-combo-box": "^5.0.10",
|
||||||
"@vaadin/vaadin-date-picker": "^4.0.7",
|
"@vaadin/vaadin-date-picker": "^4.0.7",
|
||||||
"@vue/web-component-wrapper": "^1.2.0",
|
"@vue/web-component-wrapper": "^1.2.0",
|
||||||
@@ -83,6 +89,7 @@
|
|||||||
"codemirror": "^5.49.0",
|
"codemirror": "^5.49.0",
|
||||||
"comlink": "^4.3.0",
|
"comlink": "^4.3.0",
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
|
"cropperjs": "^1.5.7",
|
||||||
"deep-clone-simple": "^1.1.1",
|
"deep-clone-simple": "^1.1.1",
|
||||||
"deep-freeze": "^0.0.1",
|
"deep-freeze": "^0.0.1",
|
||||||
"es6-object-assign": "^1.1.0",
|
"es6-object-assign": "^1.1.0",
|
||||||
@@ -99,16 +106,17 @@
|
|||||||
"lit-element": "^2.3.1",
|
"lit-element": "^2.3.1",
|
||||||
"lit-html": "^1.2.1",
|
"lit-html": "^1.2.1",
|
||||||
"lit-virtualizer": "^0.4.2",
|
"lit-virtualizer": "^0.4.2",
|
||||||
"marked": "^0.6.1",
|
"marked": "^1.1.1",
|
||||||
"mdn-polyfills": "^5.16.0",
|
"mdn-polyfills": "^5.16.0",
|
||||||
"memoize-one": "^5.0.2",
|
"memoize-one": "^5.0.2",
|
||||||
"node-vibrant": "^3.1.5",
|
"node-vibrant": "^3.1.5",
|
||||||
"proxy-polyfill": "^0.3.1",
|
"proxy-polyfill": "^0.3.1",
|
||||||
|
"punycode": "^2.1.1",
|
||||||
"regenerator-runtime": "^0.13.2",
|
"regenerator-runtime": "^0.13.2",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"roboto-fontface": "^0.10.0",
|
"roboto-fontface": "^0.10.0",
|
||||||
"superstruct": "^0.6.1",
|
"sortablejs": "^1.10.2",
|
||||||
"svg-gauge": "^1.0.6",
|
"superstruct": "^0.10.12",
|
||||||
"unfetch": "^4.1.0",
|
"unfetch": "^4.1.0",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
"vue2-daterange-picker": "^0.5.1",
|
"vue2-daterange-picker": "^0.5.1",
|
||||||
@@ -136,13 +144,14 @@
|
|||||||
"@rollup/plugin-replace": "^2.3.2",
|
"@rollup/plugin-replace": "^2.3.2",
|
||||||
"@types/chai": "^4.1.7",
|
"@types/chai": "^4.1.7",
|
||||||
"@types/chromecast-caf-receiver": "^3.0.12",
|
"@types/chromecast-caf-receiver": "^3.0.12",
|
||||||
"@types/codemirror": "^0.0.78",
|
"@types/codemirror": "^0.0.97",
|
||||||
"@types/hls.js": "^0.12.3",
|
"@types/hls.js": "^0.12.3",
|
||||||
"@types/js-yaml": "^3.12.1",
|
"@types/js-yaml": "^3.12.1",
|
||||||
"@types/leaflet": "^1.4.3",
|
"@types/leaflet": "^1.4.3",
|
||||||
"@types/leaflet-draw": "^1.0.1",
|
"@types/leaflet-draw": "^1.0.1",
|
||||||
|
"@types/marked": "^1.1.0",
|
||||||
"@types/memoize-one": "4.1.0",
|
"@types/memoize-one": "4.1.0",
|
||||||
"@types/mocha": "^5.2.6",
|
"@types/mocha": "^7.0.2",
|
||||||
"@types/resize-observer-browser": "^0.1.3",
|
"@types/resize-observer-browser": "^0.1.3",
|
||||||
"@types/webspeechapi": "^0.0.29",
|
"@types/webspeechapi": "^0.0.29",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.28.0",
|
"@typescript-eslint/eslint-plugin": "^2.28.0",
|
||||||
@@ -153,7 +162,7 @@
|
|||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"eslint-config-airbnb-typescript": "^7.2.1",
|
"eslint-config-airbnb-typescript": "^7.2.1",
|
||||||
"eslint-config-prettier": "^6.10.1",
|
"eslint-config-prettier": "^6.10.1",
|
||||||
"eslint-import-resolver-webpack": "^0.12.1",
|
"eslint-import-resolver-webpack": "^0.12.2",
|
||||||
"eslint-plugin-disable": "^2.0.1",
|
"eslint-plugin-disable": "^2.0.1",
|
||||||
"eslint-plugin-import": "^2.20.2",
|
"eslint-plugin-import": "^2.20.2",
|
||||||
"eslint-plugin-lit": "^1.2.0",
|
"eslint-plugin-lit": "^1.2.0",
|
||||||
@@ -171,12 +180,12 @@
|
|||||||
"html-minifier": "^4.0.0",
|
"html-minifier": "^4.0.0",
|
||||||
"husky": "^1.3.1",
|
"husky": "^1.3.1",
|
||||||
"lint-staged": "^8.1.5",
|
"lint-staged": "^8.1.5",
|
||||||
"lit-analyzer": "^1.1.10",
|
"lit-analyzer": "^1.2.0",
|
||||||
"lodash.template": "^4.5.0",
|
"lodash.template": "^4.5.0",
|
||||||
"magic-string": "^0.25.7",
|
"magic-string": "^0.25.7",
|
||||||
"map-stream": "^0.0.7",
|
"map-stream": "^0.0.7",
|
||||||
"merge-stream": "^1.0.1",
|
"merge-stream": "^1.0.1",
|
||||||
"mocha": "^6.0.2",
|
"mocha": "^7.2.0",
|
||||||
"object-hash": "^2.0.3",
|
"object-hash": "^2.0.3",
|
||||||
"open": "^7.0.4",
|
"open": "^7.0.4",
|
||||||
"prettier": "^2.0.4",
|
"prettier": "^2.0.4",
|
||||||
@@ -192,8 +201,8 @@
|
|||||||
"source-map-url": "^0.4.0",
|
"source-map-url": "^0.4.0",
|
||||||
"systemjs": "^6.3.2",
|
"systemjs": "^6.3.2",
|
||||||
"terser-webpack-plugin": "^3.0.6",
|
"terser-webpack-plugin": "^3.0.6",
|
||||||
"ts-lit-plugin": "^1.1.10",
|
"ts-lit-plugin": "^1.2.0",
|
||||||
"ts-mocha": "^6.0.0",
|
"ts-mocha": "^7.0.0",
|
||||||
"typescript": "^3.8.3",
|
"typescript": "^3.8.3",
|
||||||
"vinyl-buffer": "^1.0.1",
|
"vinyl-buffer": "^1.0.1",
|
||||||
"vinyl-source-stream": "^2.0.0",
|
"vinyl-source-stream": "^2.0.0",
|
||||||
@@ -210,7 +219,11 @@
|
|||||||
"@webcomponents/webcomponentsjs": "^2.2.10",
|
"@webcomponents/webcomponentsjs": "^2.2.10",
|
||||||
"@polymer/polymer": "3.1.0",
|
"@polymer/polymer": "3.1.0",
|
||||||
"lit-html": "1.2.1",
|
"lit-html": "1.2.1",
|
||||||
"lit-element": "2.3.1"
|
"lit-element": "2.3.1",
|
||||||
|
"@material/animation": "8.0.0-canary.096a7a066.0",
|
||||||
|
"@material/base": "8.0.0-canary.096a7a066.0",
|
||||||
|
"@material/feature-targeting": "8.0.0-canary.096a7a066.0",
|
||||||
|
"@material/theme": "8.0.0-canary.096a7a066.0"
|
||||||
},
|
},
|
||||||
"main": "src/home-assistant.js",
|
"main": "src/home-assistant.js",
|
||||||
"husky": {
|
"husky": {
|
||||||
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="home-assistant-frontend",
|
name="home-assistant-frontend",
|
||||||
version="20200715.1",
|
version="20200912.0",
|
||||||
description="The Home Assistant frontend",
|
description="The Home Assistant frontend",
|
||||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||||
author="The Home Assistant Authors",
|
author="The Home Assistant Authors",
|
||||||
|
@@ -16,6 +16,7 @@ import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
|||||||
import { registerServiceWorker } from "../util/register-service-worker";
|
import { registerServiceWorker } from "../util/register-service-worker";
|
||||||
import "./ha-auth-flow";
|
import "./ha-auth-flow";
|
||||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||||
|
import punycode from "punycode";
|
||||||
|
|
||||||
import(/* webpackChunkName: "pick-auth-provider" */ "./ha-pick-auth-provider");
|
import(/* webpackChunkName: "pick-auth-provider" */ "./ha-pick-auth-provider");
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||||||
${this.localize(
|
${this.localize(
|
||||||
"ui.panel.page-authorize.authorizing_client",
|
"ui.panel.page-authorize.authorizing_client",
|
||||||
"clientId",
|
"clientId",
|
||||||
this.clientId
|
this.clientId ? punycode.toASCII(this.clientId) : this.clientId
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
${loggingInWith}
|
${loggingInWith}
|
||||||
|
@@ -1,4 +1,8 @@
|
|||||||
/* eslint-disable no-undef, no-console */
|
/* eslint-disable no-undef, no-console */
|
||||||
|
import {
|
||||||
|
CastStateEventData,
|
||||||
|
SessionStateEventData,
|
||||||
|
} from "chromecast-caf-receiver/cast.framework";
|
||||||
import { Auth } from "home-assistant-js-websocket";
|
import { Auth } from "home-assistant-js-websocket";
|
||||||
import { castApiAvailable } from "./cast_framework";
|
import { castApiAvailable } from "./cast_framework";
|
||||||
import { CAST_APP_ID, CAST_DEV, CAST_NS } from "./const";
|
import { CAST_APP_ID, CAST_DEV, CAST_NS } from "./const";
|
||||||
@@ -40,16 +44,13 @@ export class CastManager {
|
|||||||
const context = this.castContext;
|
const context = this.castContext;
|
||||||
context.setOptions({
|
context.setOptions({
|
||||||
receiverApplicationId: CAST_APP_ID,
|
receiverApplicationId: CAST_APP_ID,
|
||||||
// @ts-ignore
|
|
||||||
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
|
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
|
||||||
});
|
});
|
||||||
context.addEventListener(
|
context.addEventListener(
|
||||||
// @ts-ignore
|
|
||||||
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
||||||
(ev) => this._sessionStateChanged(ev)
|
(ev) => this._sessionStateChanged(ev)
|
||||||
);
|
);
|
||||||
context.addEventListener(
|
context.addEventListener(
|
||||||
// @ts-ignore
|
|
||||||
cast.framework.CastContextEventType.CAST_STATE_CHANGED,
|
cast.framework.CastContextEventType.CAST_STATE_CHANGED,
|
||||||
(ev) => this._castStateChanged(ev)
|
(ev) => this._castStateChanged(ev)
|
||||||
);
|
);
|
||||||
@@ -118,7 +119,7 @@ export class CastManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _sessionStateChanged(ev) {
|
private _sessionStateChanged(ev: SessionStateEventData) {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
console.log("Cast session state changed", ev.sessionState);
|
console.log("Cast session state changed", ev.sessionState);
|
||||||
}
|
}
|
||||||
@@ -141,7 +142,7 @@ export class CastManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _castStateChanged(ev) {
|
private _castStateChanged(ev: CastStateEventData) {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
console.log("Cast state changed", ev.castState);
|
console.log("Cast state changed", ev.castState);
|
||||||
}
|
}
|
||||||
|
113
src/common/color/convert-color.ts
Normal file
113
src/common/color/convert-color.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
const expand_hex = (hex: string): string => {
|
||||||
|
let result = "";
|
||||||
|
for (const val of hex) {
|
||||||
|
result += val + val;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rgb_hex = (component: number): string => {
|
||||||
|
const hex = Math.round(Math.min(Math.max(component, 0), 255)).toString(16);
|
||||||
|
return hex.length === 1 ? `0${hex}` : hex;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Conversion between HEX and RGB
|
||||||
|
|
||||||
|
export const hex2rgb = (hex: string): [number, number, number] => {
|
||||||
|
hex = hex.replace("#", "");
|
||||||
|
if (hex.length === 3 || hex.length === 4) {
|
||||||
|
hex = expand_hex(hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
parseInt(hex.substring(0, 2), 16),
|
||||||
|
parseInt(hex.substring(2, 4), 16),
|
||||||
|
parseInt(hex.substring(4, 6), 16),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rgb2hex = (rgb: [number, number, number]): string => {
|
||||||
|
return `#${rgb_hex(rgb[0])}${rgb_hex(rgb[1])}${rgb_hex(rgb[2])}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Conversion between LAB, XYZ and RGB from https://github.com/gka/chroma.js
|
||||||
|
// Copyright (c) 2011-2019, Gregor Aisch
|
||||||
|
|
||||||
|
// Constants for XYZ and LAB conversion
|
||||||
|
const Xn = 0.95047;
|
||||||
|
const Yn = 1;
|
||||||
|
const Zn = 1.08883;
|
||||||
|
|
||||||
|
const t0 = 0.137931034; // 4 / 29
|
||||||
|
const t1 = 0.206896552; // 6 / 29
|
||||||
|
const t2 = 0.12841855; // 3 * t1 * t1
|
||||||
|
const t3 = 0.008856452; // t1 * t1 * t1
|
||||||
|
|
||||||
|
const rgb_xyz = (r: number) => {
|
||||||
|
r /= 255;
|
||||||
|
if (r <= 0.04045) {
|
||||||
|
return r / 12.92;
|
||||||
|
}
|
||||||
|
return ((r + 0.055) / 1.055) ** 2.4;
|
||||||
|
};
|
||||||
|
|
||||||
|
const xyz_lab = (t: number) => {
|
||||||
|
if (t > t3) {
|
||||||
|
return t ** (1 / 3);
|
||||||
|
}
|
||||||
|
return t / t2 + t0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const xyz_rgb = (r: number) => {
|
||||||
|
return 255 * (r <= 0.00304 ? 12.92 * r : 1.055 * r ** (1 / 2.4) - 0.055);
|
||||||
|
};
|
||||||
|
|
||||||
|
const lab_xyz = (t: number) => {
|
||||||
|
return t > t1 ? t * t * t : t2 * (t - t0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Conversions between RGB and LAB
|
||||||
|
|
||||||
|
const rgb2xyz = (rgb: [number, number, number]): [number, number, number] => {
|
||||||
|
let [r, g, b] = rgb;
|
||||||
|
r = rgb_xyz(r);
|
||||||
|
g = rgb_xyz(g);
|
||||||
|
b = rgb_xyz(b);
|
||||||
|
const x = xyz_lab((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / Xn);
|
||||||
|
const y = xyz_lab((0.2126729 * r + 0.7151522 * g + 0.072175 * b) / Yn);
|
||||||
|
const z = xyz_lab((0.0193339 * r + 0.119192 * g + 0.9503041 * b) / Zn);
|
||||||
|
return [x, y, z];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rgb2lab = (
|
||||||
|
rgb: [number, number, number]
|
||||||
|
): [number, number, number] => {
|
||||||
|
const [x, y, z] = rgb2xyz(rgb);
|
||||||
|
const l = 116 * y - 16;
|
||||||
|
return [l < 0 ? 0 : l, 500 * (x - y), 200 * (y - z)];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const lab2rgb = (
|
||||||
|
lab: [number, number, number]
|
||||||
|
): [number, number, number] => {
|
||||||
|
const [l, a, b] = lab;
|
||||||
|
|
||||||
|
let y = (l + 16) / 116;
|
||||||
|
let x = isNaN(a) ? y : y + a / 500;
|
||||||
|
let z = isNaN(b) ? y : y - b / 200;
|
||||||
|
|
||||||
|
y = Yn * lab_xyz(y);
|
||||||
|
x = Xn * lab_xyz(x);
|
||||||
|
z = Zn * lab_xyz(z);
|
||||||
|
|
||||||
|
const r = xyz_rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z); // D65 -> sRGB
|
||||||
|
const g = xyz_rgb(-0.969266 * x + 1.8760108 * y + 0.041556 * z);
|
||||||
|
const b_ = xyz_rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z);
|
||||||
|
|
||||||
|
return [r, g, b_];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const lab2hex = (lab: [number, number, number]): string => {
|
||||||
|
const rgb = lab2rgb(lab);
|
||||||
|
return rgb2hex(rgb);
|
||||||
|
};
|
16
src/common/color/lab.ts
Normal file
16
src/common/color/lab.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// From https://github.com/gka/chroma.js
|
||||||
|
// Copyright (c) 2011-2019, Gregor Aisch
|
||||||
|
|
||||||
|
export const labDarken = (
|
||||||
|
lab: [number, number, number],
|
||||||
|
amount = 1
|
||||||
|
): [number, number, number] => {
|
||||||
|
return [lab[0] - 18 * amount, lab[1], lab[2]];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const labBrighten = (
|
||||||
|
lab: [number, number, number],
|
||||||
|
amount = 1
|
||||||
|
): [number, number, number] => {
|
||||||
|
return labDarken(lab, -amount);
|
||||||
|
};
|
24
src/common/color/rgb.ts
Normal file
24
src/common/color/rgb.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const luminosity = (rgb: [number, number, number]): number => {
|
||||||
|
// http://www.w3.org/TR/WCAG20/#relativeluminancedef
|
||||||
|
const lum: [number, number, number] = [0, 0, 0];
|
||||||
|
for (let i = 0; i < rgb.length; i++) {
|
||||||
|
const chan = rgb[i] / 255;
|
||||||
|
lum[i] = chan <= 0.03928 ? chan / 12.92 : ((chan + 0.055) / 1.055) ** 2.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rgbContrast = (
|
||||||
|
color1: [number, number, number],
|
||||||
|
color2: [number, number, number]
|
||||||
|
) => {
|
||||||
|
const lum1 = luminosity(color1);
|
||||||
|
const lum2 = luminosity(color2);
|
||||||
|
|
||||||
|
if (lum1 > lum2) {
|
||||||
|
return (lum1 + 0.05) / (lum2 + 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (lum2 + 0.05) / (lum1 + 0.05);
|
||||||
|
};
|
9
src/common/config/components_with_service.ts
Normal file
9
src/common/config/components_with_service.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
|
||||||
|
/** Return an array of domains with the service. */
|
||||||
|
export const componentsWithService = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
service: string
|
||||||
|
): Array<string> =>
|
||||||
|
hass &&
|
||||||
|
Object.keys(hass.services).filter((key) => service in hass.services[key]);
|
9
src/common/config/is_service_loaded.ts
Normal file
9
src/common/config/is_service_loaded.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
|
||||||
|
/** Return if a service is loaded. */
|
||||||
|
export const isServiceLoaded = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
domain: string,
|
||||||
|
service: string
|
||||||
|
): boolean =>
|
||||||
|
hass && domain in hass.services && service in hass.services[domain];
|
@@ -44,7 +44,6 @@ export const DOMAINS_WITH_MORE_INFO = [
|
|||||||
"script",
|
"script",
|
||||||
"sun",
|
"sun",
|
||||||
"timer",
|
"timer",
|
||||||
"updater",
|
|
||||||
"vacuum",
|
"vacuum",
|
||||||
"water_heater",
|
"water_heater",
|
||||||
"weather",
|
"weather",
|
||||||
|
@@ -21,6 +21,11 @@ export default function relativeTime(
|
|||||||
const tense = delta >= 0 ? "past" : "future";
|
const tense = delta >= 0 ? "past" : "future";
|
||||||
delta = Math.abs(delta);
|
delta = Math.abs(delta);
|
||||||
let roundedDelta = Math.round(delta);
|
let roundedDelta = Math.round(delta);
|
||||||
|
|
||||||
|
if (roundedDelta === 0) {
|
||||||
|
return localize("ui.components.relative_time.just_now");
|
||||||
|
}
|
||||||
|
|
||||||
let unit = "week";
|
let unit = "week";
|
||||||
|
|
||||||
for (let i = 0; i < tests.length; i++) {
|
for (let i = 0; i < tests.length; i++) {
|
||||||
|
155
src/common/decorators/local-storage.ts
Normal file
155
src/common/decorators/local-storage.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import { PropertyDeclaration, UpdatingElement } from "lit-element";
|
||||||
|
import type { ClassElement } from "../../types";
|
||||||
|
|
||||||
|
type Callback = (oldValue: any, newValue: any) => void;
|
||||||
|
|
||||||
|
class Storage {
|
||||||
|
constructor() {
|
||||||
|
window.addEventListener("storage", (ev: StorageEvent) => {
|
||||||
|
if (ev.key && this.hasKey(ev.key)) {
|
||||||
|
this._storage[ev.key] = ev.newValue
|
||||||
|
? JSON.parse(ev.newValue)
|
||||||
|
: ev.newValue;
|
||||||
|
if (this._listeners[ev.key]) {
|
||||||
|
this._listeners[ev.key].forEach((listener) =>
|
||||||
|
listener(
|
||||||
|
ev.oldValue ? JSON.parse(ev.oldValue) : ev.oldValue,
|
||||||
|
this._storage[ev.key!]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _storage: { [storageKey: string]: any } = {};
|
||||||
|
|
||||||
|
private _listeners: {
|
||||||
|
[storageKey: string]: Callback[];
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
public addFromStorage(storageKey: any): void {
|
||||||
|
if (!this._storage[storageKey]) {
|
||||||
|
const data = window.localStorage.getItem(storageKey);
|
||||||
|
if (data) {
|
||||||
|
this._storage[storageKey] = JSON.parse(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscribeChanges(
|
||||||
|
storageKey: string,
|
||||||
|
callback: Callback
|
||||||
|
): UnsubscribeFunc {
|
||||||
|
if (this._listeners[storageKey]) {
|
||||||
|
this._listeners[storageKey].push(callback);
|
||||||
|
} else {
|
||||||
|
this._listeners[storageKey] = [callback];
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
this.unsubscribeChanges(storageKey, callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsubscribeChanges(storageKey: string, callback: Callback) {
|
||||||
|
if (!(storageKey in this._listeners)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = this._listeners[storageKey].indexOf(callback);
|
||||||
|
if (index !== -1) {
|
||||||
|
this._listeners[storageKey].splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasKey(storageKey: string): any {
|
||||||
|
return storageKey in this._storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getValue(storageKey: string): any {
|
||||||
|
return this._storage[storageKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
public setValue(storageKey: string, value: any): any {
|
||||||
|
this._storage[storageKey] = value;
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(storageKey, JSON.stringify(value));
|
||||||
|
} catch (err) {
|
||||||
|
// Safari in private mode doesn't allow localstorage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const storage = new Storage();
|
||||||
|
|
||||||
|
export const LocalStorage = (
|
||||||
|
storageKey?: string,
|
||||||
|
property?: boolean,
|
||||||
|
propertyOptions?: PropertyDeclaration
|
||||||
|
): any => {
|
||||||
|
return (clsElement: ClassElement) => {
|
||||||
|
const key = String(clsElement.key);
|
||||||
|
storageKey = storageKey || String(clsElement.key);
|
||||||
|
const initVal = clsElement.initializer
|
||||||
|
? clsElement.initializer()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
storage.addFromStorage(storageKey);
|
||||||
|
|
||||||
|
const subscribe = (el: UpdatingElement): UnsubscribeFunc =>
|
||||||
|
storage.subscribeChanges(storageKey!, (oldValue) => {
|
||||||
|
el.requestUpdate(clsElement.key, oldValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getValue = (): any => {
|
||||||
|
return storage.hasKey(storageKey!)
|
||||||
|
? storage.getValue(storageKey!)
|
||||||
|
: initVal;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setValue = (el: UpdatingElement, value: any) => {
|
||||||
|
let oldValue: unknown | undefined;
|
||||||
|
if (property) {
|
||||||
|
oldValue = getValue();
|
||||||
|
}
|
||||||
|
storage.setValue(storageKey!, value);
|
||||||
|
if (property) {
|
||||||
|
el.requestUpdate(clsElement.key, oldValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: "method",
|
||||||
|
placement: "prototype",
|
||||||
|
key: clsElement.key,
|
||||||
|
descriptor: {
|
||||||
|
set(this: UpdatingElement, value: unknown) {
|
||||||
|
setValue(this, value);
|
||||||
|
},
|
||||||
|
get() {
|
||||||
|
return getValue();
|
||||||
|
},
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
},
|
||||||
|
finisher(cls: typeof UpdatingElement) {
|
||||||
|
if (property) {
|
||||||
|
const connectedCallback = cls.prototype.connectedCallback;
|
||||||
|
const disconnectedCallback = cls.prototype.disconnectedCallback;
|
||||||
|
cls.prototype.connectedCallback = function () {
|
||||||
|
connectedCallback.call(this);
|
||||||
|
this[`__unbsubLocalStorage${key}`] = subscribe(this);
|
||||||
|
};
|
||||||
|
cls.prototype.disconnectedCallback = function () {
|
||||||
|
disconnectedCallback.call(this);
|
||||||
|
this[`__unbsubLocalStorage${key}`]();
|
||||||
|
};
|
||||||
|
cls.createProperty(clsElement.key, {
|
||||||
|
noAccessor: true,
|
||||||
|
...propertyOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
@@ -1,26 +1,20 @@
|
|||||||
import { derivedStyles } from "../../resources/styles";
|
import { derivedStyles, darkStyles } from "../../resources/styles";
|
||||||
import { HomeAssistant, Theme } from "../../types";
|
import { HomeAssistant, Theme } from "../../types";
|
||||||
|
import {
|
||||||
|
hex2rgb,
|
||||||
|
rgb2hex,
|
||||||
|
rgb2lab,
|
||||||
|
lab2rgb,
|
||||||
|
lab2hex,
|
||||||
|
} from "../color/convert-color";
|
||||||
|
import { rgbContrast } from "../color/rgb";
|
||||||
|
import { labDarken, labBrighten } from "../color/lab";
|
||||||
|
|
||||||
interface ProcessedTheme {
|
interface ProcessedTheme {
|
||||||
keys: { [key: string]: "" };
|
keys: { [key: string]: "" };
|
||||||
styles: { [key: string]: string };
|
styles: { [key: string]: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
const hexToRgb = (hex: string): string | null => {
|
|
||||||
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
|
||||||
const checkHex = hex.replace(shorthandRegex, (_m, r, g, b) => {
|
|
||||||
return r + r + g + g + b + b;
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(checkHex);
|
|
||||||
return result
|
|
||||||
? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(
|
|
||||||
result[3],
|
|
||||||
16
|
|
||||||
)}`
|
|
||||||
: null;
|
|
||||||
};
|
|
||||||
|
|
||||||
let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
|
let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,17 +27,56 @@ let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
|
|||||||
export const applyThemesOnElement = (
|
export const applyThemesOnElement = (
|
||||||
element,
|
element,
|
||||||
themes: HomeAssistant["themes"],
|
themes: HomeAssistant["themes"],
|
||||||
selectedTheme?: string
|
selectedTheme?: string,
|
||||||
|
themeOptions?: Partial<HomeAssistant["selectedTheme"]>
|
||||||
) => {
|
) => {
|
||||||
const newTheme = selectedTheme
|
let cacheKey = selectedTheme;
|
||||||
? PROCESSED_THEMES[selectedTheme] || processTheme(selectedTheme, themes)
|
let themeRules: Partial<Theme> = {};
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (!element._themes && !newTheme) {
|
if (selectedTheme === "default" && themeOptions) {
|
||||||
|
if (themeOptions.dark) {
|
||||||
|
cacheKey = `${cacheKey}__dark`;
|
||||||
|
themeRules = darkStyles;
|
||||||
|
}
|
||||||
|
if (themeOptions.primaryColor) {
|
||||||
|
cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`;
|
||||||
|
const rgbPrimaryColor = hex2rgb(themeOptions.primaryColor);
|
||||||
|
const labPrimaryColor = rgb2lab(rgbPrimaryColor);
|
||||||
|
themeRules["primary-color"] = themeOptions.primaryColor;
|
||||||
|
const rgbLigthPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
|
||||||
|
themeRules["light-primary-color"] = rgb2hex(rgbLigthPrimaryColor);
|
||||||
|
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
|
||||||
|
themeRules["text-primary-color"] =
|
||||||
|
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||||
|
themeRules["text-light-primary-color"] =
|
||||||
|
rgbContrast(rgbLigthPrimaryColor, [33, 33, 33]) < 6
|
||||||
|
? "#fff"
|
||||||
|
: "#212121";
|
||||||
|
themeRules["state-icon-color"] = themeRules["dark-primary-color"];
|
||||||
|
}
|
||||||
|
if (themeOptions.accentColor) {
|
||||||
|
cacheKey = `${cacheKey}__accent_${themeOptions.accentColor}`;
|
||||||
|
themeRules["accent-color"] = themeOptions.accentColor;
|
||||||
|
const rgbAccentColor = hex2rgb(themeOptions.accentColor);
|
||||||
|
themeRules["text-accent-color"] =
|
||||||
|
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedTheme && themes.themes[selectedTheme]) {
|
||||||
|
themeRules = themes.themes[selectedTheme];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!element._themes && !Object.keys(themeRules).length) {
|
||||||
// No styles to reset, and no styles to set
|
// No styles to reset, and no styles to set
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newTheme =
|
||||||
|
themeRules && cacheKey
|
||||||
|
? PROCESSED_THEMES[cacheKey] || processTheme(cacheKey, themeRules)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// Add previous set keys to reset them, and new theme
|
// Add previous set keys to reset them, and new theme
|
||||||
const styles = { ...element._themes, ...newTheme?.styles };
|
const styles = { ...element._themes, ...newTheme?.styles };
|
||||||
element._themes = newTheme?.keys;
|
element._themes = newTheme?.keys;
|
||||||
@@ -58,42 +91,45 @@ export const applyThemesOnElement = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const processTheme = (
|
const processTheme = (
|
||||||
themeName: string,
|
cacheKey: string,
|
||||||
themes: HomeAssistant["themes"]
|
theme: Partial<Theme>
|
||||||
): ProcessedTheme | undefined => {
|
): ProcessedTheme | undefined => {
|
||||||
if (!themes.themes[themeName]) {
|
if (!theme || !Object.keys(theme).length) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const theme: Theme = {
|
const combinedTheme: Partial<Theme> = {
|
||||||
...derivedStyles,
|
...derivedStyles,
|
||||||
...themes.themes[themeName],
|
...theme,
|
||||||
};
|
};
|
||||||
const styles = {};
|
const styles = {};
|
||||||
const keys = {};
|
const keys = {};
|
||||||
for (const key of Object.keys(theme)) {
|
for (const key of Object.keys(combinedTheme)) {
|
||||||
const prefixedKey = `--${key}`;
|
const prefixedKey = `--${key}`;
|
||||||
const value = theme[key];
|
const value = String(combinedTheme[key]!);
|
||||||
styles[prefixedKey] = value;
|
styles[prefixedKey] = value;
|
||||||
keys[prefixedKey] = "";
|
keys[prefixedKey] = "";
|
||||||
|
|
||||||
// Try to create a rgb value for this key if it is a hex color
|
// Try to create a rgb value for this key if it is not a var
|
||||||
if (!value.startsWith("#")) {
|
if (value.startsWith("#")) {
|
||||||
// Not a hex color
|
// Can't convert non hex value
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rgbKey = `rgb-${key}`;
|
const rgbKey = `rgb-${key}`;
|
||||||
if (theme[rgbKey] !== undefined) {
|
if (combinedTheme[rgbKey] !== undefined) {
|
||||||
// Theme has it's own rgb value
|
// Theme has it's own rgb value
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const rgbValue = hexToRgb(value);
|
try {
|
||||||
if (rgbValue !== null) {
|
const rgbValue = hex2rgb(value).join(",");
|
||||||
const prefixedRgbKey = `--${rgbKey}`;
|
const prefixedRgbKey = `--${rgbKey}`;
|
||||||
styles[prefixedRgbKey] = rgbValue;
|
styles[prefixedRgbKey] = rgbValue;
|
||||||
keys[prefixedRgbKey] = "";
|
keys[prefixedRgbKey] = "";
|
||||||
|
} catch (e) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PROCESSED_THEMES[themeName] = { styles, keys };
|
PROCESSED_THEMES[cacheKey] = { styles, keys };
|
||||||
return { styles, keys };
|
return { styles, keys };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -22,9 +22,6 @@ const _load = (
|
|||||||
(element as HTMLScriptElement).async = true;
|
(element as HTMLScriptElement).async = true;
|
||||||
if (type) {
|
if (type) {
|
||||||
(element as HTMLScriptElement).type = type;
|
(element as HTMLScriptElement).type = type;
|
||||||
// https://github.com/home-assistant/frontend/pull/6328
|
|
||||||
(element as HTMLScriptElement).crossOrigin =
|
|
||||||
url.substr(0, 1) === "/" ? "use-credentials" : "anonymous";
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "link":
|
case "link":
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import type { Map } from "leaflet";
|
import type { Map, TileLayer } from "leaflet";
|
||||||
|
|
||||||
// Sets up a Leaflet map on the provided DOM element
|
// Sets up a Leaflet map on the provided DOM element
|
||||||
export type LeafletModuleType = typeof import("leaflet");
|
export type LeafletModuleType = typeof import("leaflet");
|
||||||
@@ -6,9 +6,9 @@ export type LeafletDrawModuleType = typeof import("leaflet-draw");
|
|||||||
|
|
||||||
export const setupLeafletMap = async (
|
export const setupLeafletMap = async (
|
||||||
mapElement: HTMLElement,
|
mapElement: HTMLElement,
|
||||||
darkMode = false,
|
darkMode?: boolean,
|
||||||
draw = false
|
draw = false
|
||||||
): Promise<[Map, LeafletModuleType]> => {
|
): Promise<[Map, LeafletModuleType, TileLayer]> => {
|
||||||
if (!mapElement.parentNode) {
|
if (!mapElement.parentNode) {
|
||||||
throw new Error("Cannot setup Leaflet map on disconnected element");
|
throw new Error("Cannot setup Leaflet map on disconnected element");
|
||||||
}
|
}
|
||||||
@@ -28,15 +28,28 @@ export const setupLeafletMap = async (
|
|||||||
style.setAttribute("rel", "stylesheet");
|
style.setAttribute("rel", "stylesheet");
|
||||||
mapElement.parentNode.appendChild(style);
|
mapElement.parentNode.appendChild(style);
|
||||||
map.setView([52.3731339, 4.8903147], 13);
|
map.setView([52.3731339, 4.8903147], 13);
|
||||||
createTileLayer(Leaflet, darkMode).addTo(map);
|
|
||||||
|
|
||||||
return [map, Leaflet];
|
const tileLayer = createTileLayer(Leaflet, Boolean(darkMode)).addTo(map);
|
||||||
|
|
||||||
|
return [map, Leaflet, tileLayer];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createTileLayer = (
|
export const replaceTileLayer = (
|
||||||
|
leaflet: LeafletModuleType,
|
||||||
|
map: Map,
|
||||||
|
tileLayer: TileLayer,
|
||||||
|
darkMode: boolean
|
||||||
|
): TileLayer => {
|
||||||
|
map.removeLayer(tileLayer);
|
||||||
|
tileLayer = createTileLayer(leaflet, darkMode);
|
||||||
|
tileLayer.addTo(map);
|
||||||
|
return tileLayer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTileLayer = (
|
||||||
leaflet: LeafletModuleType,
|
leaflet: LeafletModuleType,
|
||||||
darkMode: boolean
|
darkMode: boolean
|
||||||
) => {
|
): TileLayer => {
|
||||||
return leaflet.tileLayer(
|
return leaflet.tileLayer(
|
||||||
`https://{s}.basemaps.cartocdn.com/${
|
`https://{s}.basemaps.cartocdn.com/${
|
||||||
darkMode ? "dark_all" : "light_all"
|
darkMode ? "dark_all" : "light_all"
|
||||||
|
@@ -13,7 +13,7 @@ export const batteryIcon = (
|
|||||||
return "hass:battery-unknown";
|
return "hass:battery-unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
var icon = "hass:battery";
|
let icon = "hass:battery";
|
||||||
const batteryRound = Math.round(battery / 10) * 10;
|
const batteryRound = Math.round(battery / 10) * 10;
|
||||||
if (battery_charging && battery > 10) {
|
if (battery_charging && battery > 10) {
|
||||||
icon += `-charging-${batteryRound}`;
|
icon += `-charging-${batteryRound}`;
|
||||||
|
@@ -3,49 +3,51 @@ import { HassEntity } from "home-assistant-js-websocket";
|
|||||||
/** Return an icon representing a binary sensor state. */
|
/** Return an icon representing a binary sensor state. */
|
||||||
|
|
||||||
export const binarySensorIcon = (state: HassEntity) => {
|
export const binarySensorIcon = (state: HassEntity) => {
|
||||||
const activated = state.state && state.state === "off";
|
const is_off = state.state && state.state === "off";
|
||||||
switch (state.attributes.device_class) {
|
switch (state.attributes.device_class) {
|
||||||
case "battery":
|
case "battery":
|
||||||
return activated ? "hass:battery" : "hass:battery-outline";
|
return is_off ? "hass:battery" : "hass:battery-outline";
|
||||||
|
case "battery_charging":
|
||||||
|
return is_off ? "hass:battery" : "hass:battery-charging";
|
||||||
case "cold":
|
case "cold":
|
||||||
return activated ? "hass:thermometer" : "hass:snowflake";
|
return is_off ? "hass:thermometer" : "hass:snowflake";
|
||||||
case "connectivity":
|
case "connectivity":
|
||||||
return activated ? "hass:server-network-off" : "hass:server-network";
|
return is_off ? "hass:server-network-off" : "hass:server-network";
|
||||||
case "door":
|
case "door":
|
||||||
return activated ? "hass:door-closed" : "hass:door-open";
|
return is_off ? "hass:door-closed" : "hass:door-open";
|
||||||
case "garage_door":
|
case "garage_door":
|
||||||
return activated ? "hass:garage" : "hass:garage-open";
|
return is_off ? "hass:garage" : "hass:garage-open";
|
||||||
case "gas":
|
case "gas":
|
||||||
case "power":
|
case "power":
|
||||||
case "problem":
|
case "problem":
|
||||||
case "safety":
|
case "safety":
|
||||||
case "smoke":
|
case "smoke":
|
||||||
return activated ? "hass:shield-check" : "hass:alert";
|
return is_off ? "hass:shield-check" : "hass:alert";
|
||||||
case "heat":
|
case "heat":
|
||||||
return activated ? "hass:thermometer" : "hass:fire";
|
return is_off ? "hass:thermometer" : "hass:fire";
|
||||||
case "light":
|
case "light":
|
||||||
return activated ? "hass:brightness-5" : "hass:brightness-7";
|
return is_off ? "hass:brightness-5" : "hass:brightness-7";
|
||||||
case "lock":
|
case "lock":
|
||||||
return activated ? "hass:lock" : "hass:lock-open";
|
return is_off ? "hass:lock" : "hass:lock-open";
|
||||||
case "moisture":
|
case "moisture":
|
||||||
return activated ? "hass:water-off" : "hass:water";
|
return is_off ? "hass:water-off" : "hass:water";
|
||||||
case "motion":
|
case "motion":
|
||||||
return activated ? "hass:walk" : "hass:run";
|
return is_off ? "hass:walk" : "hass:run";
|
||||||
case "occupancy":
|
case "occupancy":
|
||||||
return activated ? "hass:home-outline" : "hass:home";
|
return is_off ? "hass:home-outline" : "hass:home";
|
||||||
case "opening":
|
case "opening":
|
||||||
return activated ? "hass:square" : "hass:square-outline";
|
return is_off ? "hass:square" : "hass:square-outline";
|
||||||
case "plug":
|
case "plug":
|
||||||
return activated ? "hass:power-plug-off" : "hass:power-plug";
|
return is_off ? "hass:power-plug-off" : "hass:power-plug";
|
||||||
case "presence":
|
case "presence":
|
||||||
return activated ? "hass:home-outline" : "hass:home";
|
return is_off ? "hass:home-outline" : "hass:home";
|
||||||
case "sound":
|
case "sound":
|
||||||
return activated ? "hass:music-note-off" : "hass:music-note";
|
return is_off ? "hass:music-note-off" : "hass:music-note";
|
||||||
case "vibration":
|
case "vibration":
|
||||||
return activated ? "hass:crop-portrait" : "hass:vibrate";
|
return is_off ? "hass:crop-portrait" : "hass:vibrate";
|
||||||
case "window":
|
case "window":
|
||||||
return activated ? "hass:window-closed" : "hass:window-open";
|
return is_off ? "hass:window-closed" : "hass:window-open";
|
||||||
default:
|
default:
|
||||||
return activated ? "hass:radiobox-blank" : "hass:checkbox-marked-circle";
|
return is_off ? "hass:radiobox-blank" : "hass:checkbox-marked-circle";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -5,12 +5,16 @@ import { domainIcon } from "./domain_icon";
|
|||||||
import { batteryIcon } from "./battery_icon";
|
import { batteryIcon } from "./battery_icon";
|
||||||
|
|
||||||
const fixedDeviceClassIcons = {
|
const fixedDeviceClassIcons = {
|
||||||
|
current: "hass:current-ac",
|
||||||
|
energy: "hass:flash",
|
||||||
humidity: "hass:water-percent",
|
humidity: "hass:water-percent",
|
||||||
illuminance: "hass:brightness-5",
|
illuminance: "hass:brightness-5",
|
||||||
temperature: "hass:thermometer",
|
temperature: "hass:thermometer",
|
||||||
pressure: "hass:gauge",
|
pressure: "hass:gauge",
|
||||||
power: "hass:flash",
|
power: "hass:flash",
|
||||||
|
power_factor: "hass:angle-acute",
|
||||||
signal_strength: "hass:wifi",
|
signal_strength: "hass:wifi",
|
||||||
|
voltage: "hass:sine-wave",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sensorIcon = (state: HassEntity) => {
|
export const sensorIcon = (state: HassEntity) => {
|
||||||
|
@@ -3,9 +3,10 @@ import { HomeAssistant } from "../../types";
|
|||||||
import { DOMAINS_WITH_CARD } from "../const";
|
import { DOMAINS_WITH_CARD } from "../const";
|
||||||
import { canToggleState } from "./can_toggle_state";
|
import { canToggleState } from "./can_toggle_state";
|
||||||
import { computeStateDomain } from "./compute_state_domain";
|
import { computeStateDomain } from "./compute_state_domain";
|
||||||
|
import { UNAVAILABLE } from "../../data/entity";
|
||||||
|
|
||||||
export const stateCardType = (hass: HomeAssistant, stateObj: HassEntity) => {
|
export const stateCardType = (hass: HomeAssistant, stateObj: HassEntity) => {
|
||||||
if (stateObj.state === "unavailable") {
|
if (stateObj.state === UNAVAILABLE) {
|
||||||
return "display";
|
return "display";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,6 +4,6 @@ export const supportsFeature = (
|
|||||||
stateObj: HassEntity,
|
stateObj: HassEntity,
|
||||||
feature: number
|
feature: number
|
||||||
): boolean => {
|
): boolean => {
|
||||||
// eslint-disable-next-line:no-bitwise
|
// eslint-disable-next-line no-bitwise
|
||||||
return (stateObj.attributes.supported_features! & feature) !== 0;
|
return (stateObj.attributes.supported_features! & feature) !== 0;
|
||||||
};
|
};
|
||||||
|
@@ -1,7 +1,12 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import durationToSeconds from "../datetime/duration_to_seconds";
|
import durationToSeconds from "../datetime/duration_to_seconds";
|
||||||
|
|
||||||
export const timerTimeRemaining = (stateObj: HassEntity) => {
|
export const timerTimeRemaining = (
|
||||||
|
stateObj: HassEntity
|
||||||
|
): undefined | number => {
|
||||||
|
if (!stateObj.attributes.remaining) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
let timeRemaining = durationToSeconds(stateObj.attributes.remaining);
|
let timeRemaining = durationToSeconds(stateObj.attributes.remaining);
|
||||||
|
|
||||||
if (stateObj.state === "active") {
|
if (stateObj.state === "active") {
|
||||||
|
@@ -2,11 +2,3 @@ const validEntityId = /^(\w+)\.(\w+)$/;
|
|||||||
|
|
||||||
export const isValidEntityId = (entityId: string) =>
|
export const isValidEntityId = (entityId: string) =>
|
||||||
validEntityId.test(entityId);
|
validEntityId.test(entityId);
|
||||||
|
|
||||||
export const createValidEntityId = (input: string) =>
|
|
||||||
input
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\s|'|\./g, "_") // replace spaces, points and quotes with underscore
|
|
||||||
.replace(/\W/g, "") // remove not allowed chars
|
|
||||||
.replace(/_{2,}/g, "_") // replace multiple underscores with 1
|
|
||||||
.replace(/_$/, ""); // remove underscores at the end
|
|
||||||
|
14
src/common/mwc/handle-request-selected-event.ts
Normal file
14
src/common/mwc/handle-request-selected-event.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import {
|
||||||
|
ListItem,
|
||||||
|
RequestSelectedDetail,
|
||||||
|
} from "@material/mwc-list/mwc-list-item";
|
||||||
|
|
||||||
|
export const shouldHandleRequestSelectedEvent = (
|
||||||
|
ev: CustomEvent<RequestSelectedDetail>
|
||||||
|
): boolean => {
|
||||||
|
if (!ev.detail.selected || ev.detail.source !== "property") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
(ev.currentTarget as ListItem).selected = false;
|
||||||
|
return true;
|
||||||
|
};
|
@@ -1,5 +1,5 @@
|
|||||||
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
|
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
|
||||||
export const slugify = (value: string, delimiter = "-") => {
|
export const slugify = (value: string, delimiter = "_") => {
|
||||||
const a =
|
const a =
|
||||||
"àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;";
|
"àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;";
|
||||||
const b = `aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}`;
|
const b = `aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}`;
|
||||||
|
@@ -9,5 +9,9 @@ export function computeRTL(hass: HomeAssistant) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function computeRTLDirection(hass: HomeAssistant) {
|
export function computeRTLDirection(hass: HomeAssistant) {
|
||||||
return computeRTL(hass) ? "rtl" : "ltr";
|
return emitRTLDirection(computeRTL(hass));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitRTLDirection(rtl: boolean) {
|
||||||
|
return rtl ? "rtl" : "ltr";
|
||||||
}
|
}
|
||||||
|
50
src/common/util/throttle.ts
Normal file
50
src/common/util/throttle.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// From: underscore.js https://github.com/jashkenas/underscore/blob/master/underscore.js
|
||||||
|
|
||||||
|
// Returns a function, that, when invoked, will only be triggered at most once
|
||||||
|
// during a given window of time. Normally, the throttled function will run
|
||||||
|
// as much as it can, without ever going more than once per `wait` duration;
|
||||||
|
// but if you'd like to disable the execution on the leading edge, pass
|
||||||
|
// `false for leading`. To disable execution on the trailing edge, ditto.
|
||||||
|
export const throttle = <T extends Function>(
|
||||||
|
func: T,
|
||||||
|
wait: number,
|
||||||
|
leading = true,
|
||||||
|
trailing = true
|
||||||
|
): T => {
|
||||||
|
let timeout: number | undefined;
|
||||||
|
let previous = 0;
|
||||||
|
let context: any;
|
||||||
|
let args: any;
|
||||||
|
const later = () => {
|
||||||
|
previous = leading === false ? 0 : Date.now();
|
||||||
|
timeout = undefined;
|
||||||
|
func.apply(context, args);
|
||||||
|
if (!timeout) {
|
||||||
|
context = null;
|
||||||
|
args = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// @ts-ignore
|
||||||
|
return function (...argmnts) {
|
||||||
|
// @ts-ignore
|
||||||
|
// @typescript-eslint/no-this-alias
|
||||||
|
context = this;
|
||||||
|
args = argmnts;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (!previous && leading === false) {
|
||||||
|
previous = now;
|
||||||
|
}
|
||||||
|
const remaining = wait - (now - previous);
|
||||||
|
if (remaining <= 0 || remaining > wait) {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = undefined;
|
||||||
|
}
|
||||||
|
previous = now;
|
||||||
|
func.apply(context, args);
|
||||||
|
} else if (!timeout && trailing !== false) {
|
||||||
|
timeout = window.setTimeout(later, remaining);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
@@ -54,8 +54,8 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
|
|||||||
callService() {
|
callService() {
|
||||||
this.progress = true;
|
this.progress = true;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
var el = this;
|
const el = this;
|
||||||
var eventData = {
|
const eventData = {
|
||||||
domain: this.domain,
|
domain: this.domain,
|
||||||
service: this.service,
|
service: this.service,
|
||||||
serviceData: this.serviceData,
|
serviceData: this.serviceData,
|
||||||
|
@@ -1,110 +0,0 @@
|
|||||||
import "@material/mwc-button";
|
|
||||||
import "../ha-circular-progress";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
/* eslint-plugin-disable lit */
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
|
|
||||||
class HaProgressButton extends PolymerElement {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
:host {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
mwc-button {
|
|
||||||
transition: all 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success mwc-button {
|
|
||||||
--mdc-theme-primary: white;
|
|
||||||
background-color: var(--success-color);
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error mwc-button {
|
|
||||||
--mdc-theme-primary: white;
|
|
||||||
background-color: var(--error-color);
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
|
||||||
@apply --layout;
|
|
||||||
@apply --layout-center-center;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="container" id="container">
|
|
||||||
<mwc-button
|
|
||||||
id="button"
|
|
||||||
disabled="[[computeDisabled(disabled, progress)]]"
|
|
||||||
on-click="buttonTapped"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
</mwc-button>
|
|
||||||
<template is="dom-if" if="[[progress]]">
|
|
||||||
<div class="progress">
|
|
||||||
<ha-circular-progress active size="small"></ha-circular-progress>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
|
|
||||||
progress: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
tempClass(className) {
|
|
||||||
var classList = this.$.container.classList;
|
|
||||||
classList.add(className);
|
|
||||||
setTimeout(() => {
|
|
||||||
classList.remove(className);
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
ready() {
|
|
||||||
super.ready();
|
|
||||||
this.addEventListener("click", (ev) => this.buttonTapped(ev));
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonTapped(ev) {
|
|
||||||
if (this.progress) ev.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
actionSuccess() {
|
|
||||||
this.tempClass("success");
|
|
||||||
}
|
|
||||||
|
|
||||||
actionError() {
|
|
||||||
this.tempClass("error");
|
|
||||||
}
|
|
||||||
|
|
||||||
computeDisabled(disabled, progress) {
|
|
||||||
return disabled || progress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("ha-progress-button", HaProgressButton);
|
|
114
src/components/buttons/ha-progress-button.ts
Normal file
114
src/components/buttons/ha-progress-button.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
|
import type { Button } from "@material/mwc-button";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
TemplateResult,
|
||||||
|
query,
|
||||||
|
} from "lit-element";
|
||||||
|
|
||||||
|
import "../ha-circular-progress";
|
||||||
|
|
||||||
|
@customElement("ha-progress-button")
|
||||||
|
class HaProgressButton extends LitElement {
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public progress = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public raised = false;
|
||||||
|
|
||||||
|
@query("mwc-button") private _button?: Button;
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<mwc-button
|
||||||
|
?raised=${this.raised}
|
||||||
|
.disabled=${this.disabled || this.progress}
|
||||||
|
@click=${this._buttonTapped}
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</mwc-button>
|
||||||
|
${this.progress
|
||||||
|
? html`<div class="progress">
|
||||||
|
<ha-circular-progress size="small" active></ha-circular-progress>
|
||||||
|
</div>`
|
||||||
|
: ""}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public actionSuccess(): void {
|
||||||
|
this._tempClass("success");
|
||||||
|
}
|
||||||
|
|
||||||
|
public actionError(): void {
|
||||||
|
this._tempClass("error");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _tempClass(className: string): void {
|
||||||
|
this._button!.classList.add(className);
|
||||||
|
setTimeout(() => {
|
||||||
|
this._button!.classList.remove(className);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _buttonTapped(ev: Event): void {
|
||||||
|
if (this.progress) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
outline: none;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
mwc-button {
|
||||||
|
transition: all 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
mwc-button.success {
|
||||||
|
--mdc-theme-primary: white;
|
||||||
|
background-color: var(--success-color);
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
mwc-button[raised].success {
|
||||||
|
--mdc-theme-primary: var(--success-color);
|
||||||
|
--mdc-theme-on-primary: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
mwc-button.error {
|
||||||
|
--mdc-theme-primary: white;
|
||||||
|
background-color: var(--error-color);
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
mwc-button[raised].error {
|
||||||
|
--mdc-theme-primary: var(--error-color);
|
||||||
|
--mdc-theme-on-primary: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
bottom: 0;
|
||||||
|
margin-top: 4px;
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-progress-button": HaProgressButton;
|
||||||
|
}
|
||||||
|
}
|
@@ -3,19 +3,21 @@ import {
|
|||||||
css,
|
css,
|
||||||
CSSResult,
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
|
eventOptions,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
query,
|
query,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
eventOptions,
|
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { classMap } from "lit-html/directives/class-map";
|
import { classMap } from "lit-html/directives/class-map";
|
||||||
import { ifDefined } from "lit-html/directives/if-defined";
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
import { styleMap } from "lit-html/directives/style-map";
|
import { styleMap } from "lit-html/directives/style-map";
|
||||||
import { scroll } from "lit-virtualizer";
|
import { scroll } from "lit-virtualizer";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import "../../common/search/search-input";
|
import "../../common/search/search-input";
|
||||||
import { debounce } from "../../common/util/debounce";
|
import { debounce } from "../../common/util/debounce";
|
||||||
@@ -24,8 +26,6 @@ import "../ha-checkbox";
|
|||||||
import type { HaCheckbox } from "../ha-checkbox";
|
import type { HaCheckbox } from "../ha-checkbox";
|
||||||
import "../ha-icon";
|
import "../ha-icon";
|
||||||
import { filterData, sortData } from "./sort-filter";
|
import { filterData, sortData } from "./sort-filter";
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// for fire event
|
// for fire event
|
||||||
@@ -70,6 +70,7 @@ export interface DataTableColumnData extends DataTableSortColumnData {
|
|||||||
maxWidth?: string;
|
maxWidth?: string;
|
||||||
grows?: boolean;
|
grows?: boolean;
|
||||||
forceLTR?: boolean;
|
forceLTR?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataTableRowData {
|
export interface DataTableRowData {
|
||||||
@@ -98,6 +99,8 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
@property({ type: String }) public noDataText?: string;
|
@property({ type: String }) public noDataText?: string;
|
||||||
|
|
||||||
|
@property({ type: String }) public searchLabel?: string;
|
||||||
|
|
||||||
@property({ type: String }) public filter = "";
|
@property({ type: String }) public filter = "";
|
||||||
|
|
||||||
@internalProperty() private _filterable = false;
|
@internalProperty() private _filterable = false;
|
||||||
@@ -202,6 +205,7 @@ export class HaDataTable extends LitElement {
|
|||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<search-input
|
<search-input
|
||||||
@value-changed=${this._handleSearchChange}
|
@value-changed=${this._handleSearchChange}
|
||||||
|
.label=${this.searchLabel}
|
||||||
></search-input>
|
></search-input>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@@ -211,13 +215,15 @@ export class HaDataTable extends LitElement {
|
|||||||
class="mdc-data-table__table ${classMap({
|
class="mdc-data-table__table ${classMap({
|
||||||
"auto-height": this.autoHeight,
|
"auto-height": this.autoHeight,
|
||||||
})}"
|
})}"
|
||||||
|
role="table"
|
||||||
|
aria-rowcount=${this._filteredData.length}
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
height: this.autoHeight
|
height: this.autoHeight
|
||||||
? `${(this._filteredData.length || 1) * 53 + 57}px`
|
? `${(this._filteredData.length || 1) * 53 + 57}px`
|
||||||
: `calc(100% - ${this._header?.clientHeight}px)`,
|
: `calc(100% - ${this._header?.clientHeight}px)`,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div class="mdc-data-table__header-row">
|
<div class="mdc-data-table__header-row" role="row">
|
||||||
${this.selectable
|
${this.selectable
|
||||||
? html`
|
? html`
|
||||||
<div
|
<div
|
||||||
@@ -237,8 +243,10 @@ export class HaDataTable extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
${Object.entries(this.columns).map((columnEntry) => {
|
${Object.entries(this.columns).map(([key, column]) => {
|
||||||
const [key, column] = columnEntry;
|
if (column.hidden) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
const sorted = key === this._sortColumn;
|
const sorted = key === this._sortColumn;
|
||||||
const classes = {
|
const classes = {
|
||||||
"mdc-data-table__header-cell--numeric": Boolean(
|
"mdc-data-table__header-cell--numeric": Boolean(
|
||||||
@@ -285,8 +293,8 @@ export class HaDataTable extends LitElement {
|
|||||||
${!this._filteredData.length
|
${!this._filteredData.length
|
||||||
? html`
|
? html`
|
||||||
<div class="mdc-data-table__content">
|
<div class="mdc-data-table__content">
|
||||||
<div class="mdc-data-table__row">
|
<div class="mdc-data-table__row" role="row">
|
||||||
<div class="mdc-data-table__cell grows center">
|
<div class="mdc-data-table__cell grows center" role="cell">
|
||||||
${this.noDataText || "No data"}
|
${this.noDataText || "No data"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -301,12 +309,14 @@ export class HaDataTable extends LitElement {
|
|||||||
items: !this.hasFab
|
items: !this.hasFab
|
||||||
? this._filteredData
|
? this._filteredData
|
||||||
: [...this._filteredData, ...[{ empty: true }]],
|
: [...this._filteredData, ...[{ empty: true }]],
|
||||||
renderItem: (row: DataTableRowData) => {
|
renderItem: (row: DataTableRowData, index) => {
|
||||||
if (row.empty) {
|
if (row.empty) {
|
||||||
return html` <div class="mdc-data-table__row"></div> `;
|
return html` <div class="mdc-data-table__row"></div> `;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
|
aria-rowindex=${index}
|
||||||
|
role="row"
|
||||||
.rowId="${row[this.id]}"
|
.rowId="${row[this.id]}"
|
||||||
@click=${this._handleRowClick}
|
@click=${this._handleRowClick}
|
||||||
class="mdc-data-table__row ${classMap({
|
class="mdc-data-table__row ${classMap({
|
||||||
@@ -325,6 +335,7 @@ export class HaDataTable extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<div
|
<div
|
||||||
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
|
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
|
||||||
|
role="cell"
|
||||||
>
|
>
|
||||||
<ha-checkbox
|
<ha-checkbox
|
||||||
class="mdc-data-table__row-checkbox"
|
class="mdc-data-table__row-checkbox"
|
||||||
@@ -338,40 +349,45 @@ export class HaDataTable extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
${Object.entries(this.columns).map((columnEntry) => {
|
${Object.entries(this.columns).map(
|
||||||
const [key, column] = columnEntry;
|
([key, column]) => {
|
||||||
return html`
|
if (column.hidden) {
|
||||||
<div
|
return "";
|
||||||
class="mdc-data-table__cell ${classMap({
|
}
|
||||||
"mdc-data-table__cell--numeric": Boolean(
|
return html`
|
||||||
column.type === "numeric"
|
<div
|
||||||
),
|
role="cell"
|
||||||
"mdc-data-table__cell--icon": Boolean(
|
class="mdc-data-table__cell ${classMap({
|
||||||
column.type === "icon"
|
"mdc-data-table__cell--numeric": Boolean(
|
||||||
),
|
column.type === "numeric"
|
||||||
"mdc-data-table__cell--icon-button": Boolean(
|
),
|
||||||
column.type === "icon-button"
|
"mdc-data-table__cell--icon": Boolean(
|
||||||
),
|
column.type === "icon"
|
||||||
grows: Boolean(column.grows),
|
),
|
||||||
forceLTR: Boolean(column.forceLTR),
|
"mdc-data-table__cell--icon-button": Boolean(
|
||||||
})}"
|
column.type === "icon-button"
|
||||||
style=${column.width
|
),
|
||||||
? styleMap({
|
grows: Boolean(column.grows),
|
||||||
[column.grows
|
forceLTR: Boolean(column.forceLTR),
|
||||||
? "minWidth"
|
})}"
|
||||||
: "width"]: column.width,
|
style=${column.width
|
||||||
maxWidth: column.maxWidth
|
? styleMap({
|
||||||
? column.maxWidth
|
[column.grows
|
||||||
: "",
|
? "minWidth"
|
||||||
})
|
: "width"]: column.width,
|
||||||
: ""}
|
maxWidth: column.maxWidth
|
||||||
>
|
? column.maxWidth
|
||||||
${column.template
|
: "",
|
||||||
? column.template(row[key], row)
|
})
|
||||||
: row[key]}
|
: ""}
|
||||||
</div>
|
>
|
||||||
`;
|
${column.template
|
||||||
})}
|
? column.template(row[key], row)
|
||||||
|
: row[key]}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
@@ -538,7 +554,7 @@ export class HaDataTable extends LitElement {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: rgba(var(--rgb-primary-text-color), 0.12);
|
border-color: var(--divider-color);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -556,7 +572,7 @@ export class HaDataTable extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__row ~ .mdc-data-table__row {
|
.mdc-data-table__row ~ .mdc-data-table__row {
|
||||||
border-top: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
border-top: 1px solid var(--divider-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
|
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
|
||||||
@@ -575,7 +591,7 @@ export class HaDataTable extends LitElement {
|
|||||||
height: 56px;
|
height: 56px;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
border-bottom: 1px solid var(--divider-color);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -828,7 +844,7 @@ export class HaDataTable extends LitElement {
|
|||||||
right: 12px;
|
right: 12px;
|
||||||
}
|
}
|
||||||
.table-header {
|
.table-header {
|
||||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
border-bottom: 1px solid var(--divider-color);
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
}
|
}
|
||||||
search-input {
|
search-input {
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
// To use comlink under ES5
|
// To use comlink under ES5
|
||||||
import "proxy-polyfill";
|
|
||||||
import { expose } from "comlink";
|
import { expose } from "comlink";
|
||||||
|
import "proxy-polyfill";
|
||||||
import type {
|
import type {
|
||||||
DataTableSortColumnData,
|
|
||||||
DataTableRowData,
|
DataTableRowData,
|
||||||
SortingDirection,
|
DataTableSortColumnData,
|
||||||
SortableColumnContainer,
|
SortableColumnContainer,
|
||||||
|
SortingDirection,
|
||||||
} from "./ha-data-table";
|
} from "./ha-data-table";
|
||||||
|
|
||||||
const filterData = (
|
const filterData = (
|
||||||
@@ -19,7 +19,7 @@ const filterData = (
|
|||||||
const [key, column] = columnEntry;
|
const [key, column] = columnEntry;
|
||||||
if (column.filterable) {
|
if (column.filterable) {
|
||||||
if (
|
if (
|
||||||
(column.filterKey ? row[key][column.filterKey] : row[key])
|
String(column.filterKey ? row[key][column.filterKey] : row[key])
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
.includes(filter)
|
.includes(filter)
|
||||||
) {
|
) {
|
||||||
|
@@ -135,7 +135,7 @@ class DateRangePickerElement extends WrappedElement {
|
|||||||
}
|
}
|
||||||
.daterangepicker td.in-range {
|
.daterangepicker td.in-range {
|
||||||
background-color: var(--light-primary-color);
|
background-color: var(--light-primary-color);
|
||||||
color: var(--primary-text-color);
|
color: var(--text-light-primary-color, var(--primary-text-color));
|
||||||
}
|
}
|
||||||
.daterangepicker td.active,
|
.daterangepicker td.active,
|
||||||
.daterangepicker td.active:hover {
|
.daterangepicker td.active:hover {
|
||||||
|
@@ -117,11 +117,7 @@ export abstract class HaDeviceAutomationPicker<
|
|||||||
>
|
>
|
||||||
${this.NO_AUTOMATION_TEXT}
|
${this.NO_AUTOMATION_TEXT}
|
||||||
</paper-item>
|
</paper-item>
|
||||||
<paper-item
|
<paper-item key=${UNKNOWN_AUTOMATION_KEY} hidden>
|
||||||
key=${UNKNOWN_AUTOMATION_KEY}
|
|
||||||
.automation=${this.value}
|
|
||||||
hidden
|
|
||||||
>
|
|
||||||
${this.UNKNOWN_AUTOMATION_TEXT}
|
${this.UNKNOWN_AUTOMATION_TEXT}
|
||||||
</paper-item>
|
</paper-item>
|
||||||
${this._automations.map(
|
${this._automations.map(
|
||||||
@@ -175,18 +171,17 @@ export abstract class HaDeviceAutomationPicker<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _automationChanged(ev) {
|
private _automationChanged(ev) {
|
||||||
this._setValue(ev.detail.item.automation);
|
if (ev.detail.item.automation) {
|
||||||
|
this._setValue(ev.detail.item.automation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setValue(automation: T) {
|
private _setValue(automation: T) {
|
||||||
if (this.value && deviceAutomationsEqual(automation, this.value)) {
|
if (this.value && deviceAutomationsEqual(automation, this.value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.value = automation;
|
fireEvent(this, "change");
|
||||||
setTimeout(() => {
|
fireEvent(this, "value-changed", { value: automation });
|
||||||
fireEvent(this, "change");
|
|
||||||
fireEvent(this, "value-changed", { value: automation });
|
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult {
|
static get styles(): CSSResult {
|
||||||
|
@@ -23,10 +23,10 @@ export const HaIronFocusablesHelper = {
|
|||||||
* @return {!Array<!HTMLElement>}
|
* @return {!Array<!HTMLElement>}
|
||||||
*/
|
*/
|
||||||
getTabbableNodes: function (node) {
|
getTabbableNodes: function (node) {
|
||||||
var result = [];
|
const result = [];
|
||||||
// If there is at least one element with tabindex > 0, we need to sort
|
// If there is at least one element with tabindex > 0, we need to sort
|
||||||
// the final array by tabindex.
|
// the final array by tabindex.
|
||||||
var needsSortByTabIndex = this._collectTabbableNodes(node, result);
|
const needsSortByTabIndex = this._collectTabbableNodes(node, result);
|
||||||
if (needsSortByTabIndex) {
|
if (needsSortByTabIndex) {
|
||||||
return IronFocusablesHelper._sortByTabIndex(result);
|
return IronFocusablesHelper._sortByTabIndex(result);
|
||||||
}
|
}
|
||||||
@@ -50,9 +50,9 @@ export const HaIronFocusablesHelper = {
|
|||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var element = /** @type {!HTMLElement} */ (node);
|
const element = /** @type {!HTMLElement} */ (node);
|
||||||
var tabIndex = IronFocusablesHelper._normalizedTabIndex(element);
|
const tabIndex = IronFocusablesHelper._normalizedTabIndex(element);
|
||||||
var needsSort = tabIndex > 0;
|
let needsSort = tabIndex > 0;
|
||||||
if (tabIndex >= 0) {
|
if (tabIndex >= 0) {
|
||||||
result.push(element);
|
result.push(element);
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ export const HaIronFocusablesHelper = {
|
|||||||
// <input id="B" slot="b" tabindex="1">
|
// <input id="B" slot="b" tabindex="1">
|
||||||
// </div>
|
// </div>
|
||||||
// TODO(valdrin) support ShadowDOM v1 when upgrading to Polymer v2.0.
|
// TODO(valdrin) support ShadowDOM v1 when upgrading to Polymer v2.0.
|
||||||
var children;
|
let children;
|
||||||
if (element.localName === "content" || element.localName === "slot") {
|
if (element.localName === "content" || element.localName === "slot") {
|
||||||
children = dom(element).getDistributedNodes();
|
children = dom(element).getDistributedNodes();
|
||||||
} else {
|
} else {
|
||||||
@@ -80,7 +80,7 @@ export const HaIronFocusablesHelper = {
|
|||||||
children = dom(element.shadowRoot || element.root || element).children;
|
children = dom(element.shadowRoot || element.root || element).children;
|
||||||
// /////////////////////////
|
// /////////////////////////
|
||||||
}
|
}
|
||||||
for (var i = 0; i < children.length; i++) {
|
for (let i = 0; i < children.length; i++) {
|
||||||
// Ensure method is always invoked to collect tabbable children.
|
// Ensure method is always invoked to collect tabbable children.
|
||||||
needsSort = this._collectTabbableNodes(children[i], result) || needsSort;
|
needsSort = this._collectTabbableNodes(children[i], result) || needsSort;
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
/* eslint-plugin-disable lit */
|
/* eslint-plugin-disable lit */
|
||||||
import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior";
|
import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior";
|
||||||
import "../ha-icon-button";
|
|
||||||
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
|
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
|
||||||
import { timeOut } from "@polymer/polymer/lib/utils/async";
|
import { timeOut } from "@polymer/polymer/lib/utils/async";
|
||||||
import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
|
import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
import { formatTime } from "../../common/datetime/format_time";
|
import { formatTime } from "../../common/datetime/format_time";
|
||||||
|
import "../ha-icon-button";
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
/* global Chart moment Color */
|
/* global Chart moment Color */
|
||||||
@@ -355,7 +355,7 @@ class HaChartBase extends mixinBehaviors(
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
const date = new Date(values[index].value);
|
const date = new Date(values[index].value);
|
||||||
return formatTime(date);
|
return formatTime(date, this.hass.language);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawChart() {
|
drawChart() {
|
||||||
@@ -420,7 +420,7 @@ class HaChartBase extends mixinBehaviors(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
options = Chart.helpers.merge(options, this.data.options);
|
options = Chart.helpers.merge(options, this.data.options);
|
||||||
options.scales.xAxes[0].ticks.callback = this._formatTickValue;
|
options.scales.xAxes[0].ticks.callback = this._formatTickValue.bind(this);
|
||||||
if (this.data.type === "timeline") {
|
if (this.data.type === "timeline") {
|
||||||
this.set("isTimeline", true);
|
this.set("isTimeline", true);
|
||||||
if (this.data.colors !== undefined) {
|
if (this.data.colors !== undefined) {
|
||||||
|
178
src/components/entity/ha-entity-attribute-picker.ts
Normal file
178
src/components/entity/ha-entity-attribute-picker.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import "@polymer/paper-input/paper-input";
|
||||||
|
import "@polymer/paper-item/paper-item";
|
||||||
|
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
PropertyValues,
|
||||||
|
query,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { PolymerChangedEvent } from "../../polymer-types";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import "../ha-icon-button";
|
||||||
|
import "./state-badge";
|
||||||
|
|
||||||
|
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||||
|
|
||||||
|
const rowRenderer = (root: HTMLElement, _owner, model: { item: string }) => {
|
||||||
|
if (!root.firstElementChild) {
|
||||||
|
root.innerHTML = `
|
||||||
|
<style>
|
||||||
|
paper-item {
|
||||||
|
margin: -10px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<paper-item></paper-item>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
root.querySelector("paper-item")!.textContent = model.item;
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement("ha-entity-attribute-picker")
|
||||||
|
class HaEntityAttributePicker extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public entityId?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public autofocus = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "allow-custom-value" })
|
||||||
|
public allowCustomValue;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public value?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) private _opened = false;
|
||||||
|
|
||||||
|
@query("vaadin-combo-box-light") private _comboBox!: HTMLElement;
|
||||||
|
|
||||||
|
protected shouldUpdate(changedProps: PropertyValues) {
|
||||||
|
return !(!changedProps.has("_opened") && this._opened);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
if (changedProps.has("_opened") && this._opened) {
|
||||||
|
const state = this.entityId ? this.hass.states[this.entityId] : undefined;
|
||||||
|
(this._comboBox as any).items = state
|
||||||
|
? Object.keys(state.attributes)
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this.hass) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<vaadin-combo-box-light
|
||||||
|
.value=${this._value}
|
||||||
|
.allowCustomValue=${this.allowCustomValue}
|
||||||
|
.renderer=${rowRenderer}
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
>
|
||||||
|
<paper-input
|
||||||
|
.autofocus=${this.autofocus}
|
||||||
|
.label=${this.label ??
|
||||||
|
this.hass.localize(
|
||||||
|
"ui.components.entity.entity-attribute-picker.attribute"
|
||||||
|
)}
|
||||||
|
.value=${this._value}
|
||||||
|
.disabled=${this.disabled || !this.entityId}
|
||||||
|
class="input"
|
||||||
|
autocapitalize="none"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
spellcheck="false"
|
||||||
|
>
|
||||||
|
${this.value
|
||||||
|
? html`
|
||||||
|
<ha-icon-button
|
||||||
|
aria-label=${this.hass.localize(
|
||||||
|
"ui.components.entity.entity-picker.clear"
|
||||||
|
)}
|
||||||
|
slot="suffix"
|
||||||
|
class="clear-button"
|
||||||
|
icon="hass:close"
|
||||||
|
@click=${this._clearValue}
|
||||||
|
no-ripple
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</ha-icon-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
|
||||||
|
<ha-icon-button
|
||||||
|
aria-label=${this.hass.localize(
|
||||||
|
"ui.components.entity.entity-attribute-picker.show_attributes"
|
||||||
|
)}
|
||||||
|
slot="suffix"
|
||||||
|
class="toggle-button"
|
||||||
|
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
|
||||||
|
>
|
||||||
|
Toggle
|
||||||
|
</ha-icon-button>
|
||||||
|
</paper-input>
|
||||||
|
</vaadin-combo-box-light>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearValue(ev: Event) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._setValue("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _value() {
|
||||||
|
return this.value || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
|
||||||
|
this._opened = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev: PolymerChangedEvent<string>) {
|
||||||
|
const newValue = ev.detail.value;
|
||||||
|
if (newValue !== this._value) {
|
||||||
|
this._setValue(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setValue(value: string) {
|
||||||
|
this.value = value;
|
||||||
|
setTimeout(() => {
|
||||||
|
fireEvent(this, "value-changed", { value });
|
||||||
|
fireEvent(this, "change");
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
paper-input > ha-icon-button {
|
||||||
|
--mdc-icon-button-size: 24px;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-entity-attribute-picker": HaEntityAttributePicker;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,3 @@
|
|||||||
import "../ha-icon-button";
|
|
||||||
import "@polymer/paper-input/paper-input";
|
import "@polymer/paper-input/paper-input";
|
||||||
import "@polymer/paper-item/paper-icon-item";
|
import "@polymer/paper-item/paper-icon-item";
|
||||||
import "@polymer/paper-item/paper-item-body";
|
import "@polymer/paper-item/paper-item-body";
|
||||||
@@ -7,6 +6,7 @@ import { HassEntity } from "home-assistant-js-websocket";
|
|||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResult,
|
CSSResult,
|
||||||
|
customElement,
|
||||||
html,
|
html,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
@@ -20,6 +20,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
|
|||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
import { PolymerChangedEvent } from "../../polymer-types";
|
import { PolymerChangedEvent } from "../../polymer-types";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
|
import "../ha-icon-button";
|
||||||
import "./state-badge";
|
import "./state-badge";
|
||||||
|
|
||||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||||
@@ -51,7 +52,8 @@ const rowRenderer = (
|
|||||||
root.querySelector("[secondary]")!.textContent = model.item.entity_id;
|
root.querySelector("[secondary]")!.textContent = model.item.entity_id;
|
||||||
};
|
};
|
||||||
|
|
||||||
class HaEntityPicker extends LitElement {
|
@customElement("ha-entity-picker")
|
||||||
|
export class HaEntityPicker extends LitElement {
|
||||||
@property({ type: Boolean }) public autofocus = false;
|
@property({ type: Boolean }) public autofocus = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled?: boolean;
|
@property({ type: Boolean }) public disabled?: boolean;
|
||||||
@@ -95,6 +97,8 @@ class HaEntityPicker extends LitElement {
|
|||||||
|
|
||||||
@query("vaadin-combo-box-light") private _comboBox!: HTMLElement;
|
@query("vaadin-combo-box-light") private _comboBox!: HTMLElement;
|
||||||
|
|
||||||
|
private _initedStates = false;
|
||||||
|
|
||||||
private _getStates = memoizeOne(
|
private _getStates = memoizeOne(
|
||||||
(
|
(
|
||||||
_opened: boolean,
|
_opened: boolean,
|
||||||
@@ -148,11 +152,18 @@ class HaEntityPicker extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
protected shouldUpdate(changedProps: PropertyValues) {
|
protected shouldUpdate(changedProps: PropertyValues) {
|
||||||
|
if (
|
||||||
|
changedProps.has("value") ||
|
||||||
|
changedProps.has("label") ||
|
||||||
|
changedProps.has("disabled")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return !(!changedProps.has("_opened") && this._opened);
|
return !(!changedProps.has("_opened") && this._opened);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
protected updated(changedProps: PropertyValues) {
|
||||||
if (changedProps.has("_opened") && this._opened) {
|
if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
|
||||||
const states = this._getStates(
|
const states = this._getStates(
|
||||||
this._opened,
|
this._opened,
|
||||||
this.hass,
|
this.hass,
|
||||||
@@ -162,6 +173,7 @@ class HaEntityPicker extends LitElement {
|
|||||||
this.includeDeviceClasses
|
this.includeDeviceClasses
|
||||||
);
|
);
|
||||||
(this._comboBox as any).items = states;
|
(this._comboBox as any).items = states;
|
||||||
|
this._initedStates = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +181,6 @@ class HaEntityPicker extends LitElement {
|
|||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<vaadin-combo-box-light
|
<vaadin-combo-box-light
|
||||||
item-value-path="entity_id"
|
item-value-path="entity_id"
|
||||||
@@ -267,8 +278,6 @@ class HaEntityPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("ha-entity-picker", HaEntityPicker);
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"ha-entity-picker": HaEntityPicker;
|
"ha-entity-picker": HaEntityPicker;
|
||||||
|
@@ -20,6 +20,7 @@ import { stateIcon } from "../../common/entity/state_icon";
|
|||||||
import { timerTimeRemaining } from "../../common/entity/timer_time_remaining";
|
import { timerTimeRemaining } from "../../common/entity/timer_time_remaining";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "../ha-label-badge";
|
import "../ha-label-badge";
|
||||||
|
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||||
|
|
||||||
@customElement("ha-state-label-badge")
|
@customElement("ha-state-label-badge")
|
||||||
export class HaStateLabelBadge extends LitElement {
|
export class HaStateLabelBadge extends LitElement {
|
||||||
@@ -81,7 +82,8 @@ export class HaStateLabelBadge extends LitElement {
|
|||||||
? ""
|
? ""
|
||||||
: this.image
|
: this.image
|
||||||
? this.image
|
? this.image
|
||||||
: state.attributes.entity_picture}"
|
: state.attributes.entity_picture_local ||
|
||||||
|
state.attributes.entity_picture}"
|
||||||
.label="${this._computeLabel(domain, state, this._timerTimeRemaining)}"
|
.label="${this._computeLabel(domain, state, this._timerTimeRemaining)}"
|
||||||
.description="${this.name ? this.name : computeStateName(state)}"
|
.description="${this.name ? this.name : computeStateName(state)}"
|
||||||
></ha-label-badge>
|
></ha-label-badge>
|
||||||
@@ -108,7 +110,7 @@ export class HaStateLabelBadge extends LitElement {
|
|||||||
return null;
|
return null;
|
||||||
case "sensor":
|
case "sensor":
|
||||||
default:
|
default:
|
||||||
return state.state === "unknown"
|
return state.state === UNKNOWN
|
||||||
? "-"
|
? "-"
|
||||||
: state.attributes.unit_of_measurement
|
: state.attributes.unit_of_measurement
|
||||||
? state.state
|
? state.state
|
||||||
@@ -121,7 +123,7 @@ export class HaStateLabelBadge extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _computeIcon(domain: string, state: HassEntity) {
|
private _computeIcon(domain: string, state: HassEntity) {
|
||||||
if (state.state === "unavailable") {
|
if (state.state === UNAVAILABLE) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
switch (domain) {
|
switch (domain) {
|
||||||
@@ -166,7 +168,7 @@ export class HaStateLabelBadge extends LitElement {
|
|||||||
|
|
||||||
private _computeLabel(domain, state, _timerTimeRemaining) {
|
private _computeLabel(domain, state, _timerTimeRemaining) {
|
||||||
if (
|
if (
|
||||||
state.state === "unavailable" ||
|
state.state === UNAVAILABLE ||
|
||||||
["device_tracker", "alarm_control_panel", "person"].includes(domain)
|
["device_tracker", "alarm_control_panel", "person"].includes(domain)
|
||||||
) {
|
) {
|
||||||
// Localize the state with a special state_badge namespace, which has variations of
|
// Localize the state with a special state_badge namespace, which has variations of
|
||||||
|
@@ -73,10 +73,10 @@ export class StateBadge extends LitElement {
|
|||||||
if (stateObj) {
|
if (stateObj) {
|
||||||
// hide icon if we have entity picture
|
// hide icon if we have entity picture
|
||||||
if (
|
if (
|
||||||
(stateObj.attributes.entity_picture && !this.overrideIcon) ||
|
((stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture) && !this.overrideIcon) ||
|
||||||
this.overrideImage
|
this.overrideImage
|
||||||
) {
|
) {
|
||||||
let imageUrl = this.overrideImage || stateObj.attributes.entity_picture;
|
let imageUrl = this.overrideImage || stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture;
|
||||||
if (this.hass) {
|
if (this.hass) {
|
||||||
imageUrl = this.hass.hassUrl(imageUrl);
|
imageUrl = this.hass.hassUrl(imageUrl);
|
||||||
}
|
}
|
||||||
|
67
src/components/ha-bar.ts
Normal file
67
src/components/ha-bar.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
svg,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getValueInPercentage,
|
||||||
|
normalize,
|
||||||
|
roundWithOneDecimal,
|
||||||
|
} from "../util/calculate";
|
||||||
|
|
||||||
|
@customElement("ha-bar")
|
||||||
|
export class HaBar extends LitElement {
|
||||||
|
@property({ type: Number }) public min = 0;
|
||||||
|
|
||||||
|
@property({ type: Number }) public max = 100;
|
||||||
|
|
||||||
|
@property({ type: Number }) public value!: number;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const valuePrecentage = roundWithOneDecimal(
|
||||||
|
getValueInPercentage(
|
||||||
|
normalize(this.value, this.min, this.max),
|
||||||
|
this.min,
|
||||||
|
this.max
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return svg`
|
||||||
|
<svg>
|
||||||
|
<g>
|
||||||
|
<rect></rect>
|
||||||
|
<rect width="${valuePrecentage}%"></rect>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
rect:first-child {
|
||||||
|
width: 100%;
|
||||||
|
fill: var(--ha-bar-background-color, var(--secondary-background-color));
|
||||||
|
}
|
||||||
|
rect:last-child {
|
||||||
|
fill: var(--ha-bar-primary-color, var(--primary-color));
|
||||||
|
rx: var(--ha-bar-border-radius, 4px);
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
border-radius: var(--ha-bar-border-radius, 4px);
|
||||||
|
height: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-bar": HaBar;
|
||||||
|
}
|
||||||
|
}
|
@@ -10,7 +10,6 @@ import {
|
|||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import "@material/mwc-menu";
|
import "@material/mwc-menu";
|
||||||
import "@material/mwc-list/mwc-list-item";
|
|
||||||
import type { Menu, Corner } from "@material/mwc-menu";
|
import type { Menu, Corner } from "@material/mwc-menu";
|
||||||
|
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
@@ -19,14 +18,30 @@ import "./ha-icon-button";
|
|||||||
export class HaButtonMenu extends LitElement {
|
export class HaButtonMenu extends LitElement {
|
||||||
@property() public corner: Corner = "TOP_START";
|
@property() public corner: Corner = "TOP_START";
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public multi = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public activatable = false;
|
||||||
|
|
||||||
@query("mwc-menu") private _menu?: Menu;
|
@query("mwc-menu") private _menu?: Menu;
|
||||||
|
|
||||||
|
public get items() {
|
||||||
|
return this._menu?.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get selected() {
|
||||||
|
return this._menu?.selected;
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div @click=${this._handleClick}>
|
<div @click=${this._handleClick}>
|
||||||
<slot name="trigger"></slot>
|
<slot name="trigger"></slot>
|
||||||
</div>
|
</div>
|
||||||
<mwc-menu .corner=${this.corner}>
|
<mwc-menu
|
||||||
|
.corner=${this.corner}
|
||||||
|
.multi=${this.multi}
|
||||||
|
.activatable=${this.activatable}
|
||||||
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</mwc-menu>
|
</mwc-menu>
|
||||||
`;
|
`;
|
||||||
|
@@ -1,21 +1,20 @@
|
|||||||
|
import "@material/mwc-icon-button/mwc-icon-button";
|
||||||
import {
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
TemplateResult,
|
|
||||||
property,
|
|
||||||
LitElement,
|
LitElement,
|
||||||
CSSResult,
|
property,
|
||||||
css,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
|
|
||||||
import "./ha-icon-button";
|
|
||||||
|
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import type { ToggleButton } from "../types";
|
import type { ToggleButton } from "../types";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
|
||||||
@customElement("ha-button-toggle-group")
|
@customElement("ha-button-toggle-group")
|
||||||
export class HaButtonToggleGroup extends LitElement {
|
export class HaButtonToggleGroup extends LitElement {
|
||||||
@property() public buttons!: ToggleButton[];
|
@property({ attribute: false }) public buttons!: ToggleButton[];
|
||||||
|
|
||||||
@property() public active?: string;
|
@property() public active?: string;
|
||||||
|
|
||||||
@@ -23,21 +22,23 @@ export class HaButtonToggleGroup extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
${this.buttons.map(
|
${this.buttons.map(
|
||||||
(button) => html` <ha-icon-button
|
(button) => html`
|
||||||
.label=${button.label}
|
<mwc-icon-button
|
||||||
.icon=${button.icon}
|
.label=${button.label}
|
||||||
.value=${button.value}
|
.value=${button.value}
|
||||||
?active=${this.active === button.value}
|
?active=${this.active === button.value}
|
||||||
@click=${this._handleClick}
|
@click=${this._handleClick}
|
||||||
>
|
>
|
||||||
</ha-icon-button>`
|
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
|
||||||
|
</mwc-icon-button>
|
||||||
|
`
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleClick(ev): void {
|
private _handleClick(ev): void {
|
||||||
this.active = ev.target.value;
|
this.active = ev.currentTarget.value;
|
||||||
fireEvent(this, "value-changed", { value: this.active });
|
fireEvent(this, "value-changed", { value: this.active });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,12 +49,13 @@ export class HaButtonToggleGroup extends LitElement {
|
|||||||
--mdc-icon-button-size: var(--button-toggle-size, 36px);
|
--mdc-icon-button-size: var(--button-toggle-size, 36px);
|
||||||
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
|
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
|
||||||
}
|
}
|
||||||
ha-icon-button {
|
mwc-icon-button {
|
||||||
border: 1px solid var(--primary-color);
|
border: 1px solid var(--primary-color);
|
||||||
border-right-width: 0px;
|
border-right-width: 0px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
ha-icon-button::before {
|
mwc-icon-button::before {
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -65,22 +67,26 @@ export class HaButtonToggleGroup extends LitElement {
|
|||||||
content: "";
|
content: "";
|
||||||
transition: opacity 15ms linear, background-color 15ms linear;
|
transition: opacity 15ms linear, background-color 15ms linear;
|
||||||
}
|
}
|
||||||
ha-icon-button[active]::before {
|
mwc-icon-button[active]::before {
|
||||||
opacity: var(--mdc-icon-button-ripple-opacity, 0.12);
|
opacity: var(--mdc-icon-button-ripple-opacity, 0.12);
|
||||||
}
|
}
|
||||||
ha-icon-button:first-child {
|
mwc-icon-button:first-child {
|
||||||
border-radius: 4px 0 0 4px;
|
border-radius: 4px 0 0 4px;
|
||||||
}
|
}
|
||||||
ha-icon-button:last-child {
|
mwc-icon-button:last-child {
|
||||||
border-radius: 0 4px 4px 0;
|
border-radius: 0 4px 4px 0;
|
||||||
border-right-width: 1px;
|
border-right-width: 1px;
|
||||||
}
|
}
|
||||||
|
mwc-icon-button:only-child {
|
||||||
|
border-radius: 4px;
|
||||||
|
border-right-width: 1px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"ha-button-toggle-button": HaButtonToggleGroup;
|
"ha-button-toggle-group": HaButtonToggleGroup;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,9 +3,9 @@ import {
|
|||||||
CSSResult,
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
@@ -18,37 +18,28 @@ import {
|
|||||||
fetchStreamUrl,
|
fetchStreamUrl,
|
||||||
} from "../data/camera";
|
} from "../data/camera";
|
||||||
import { CameraEntity, HomeAssistant } from "../types";
|
import { CameraEntity, HomeAssistant } from "../types";
|
||||||
|
import "./ha-hls-player";
|
||||||
type HLSModule = typeof import("hls.js");
|
|
||||||
|
|
||||||
@customElement("ha-camera-stream")
|
@customElement("ha-camera-stream")
|
||||||
class HaCameraStream extends LitElement {
|
class HaCameraStream extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property() public stateObj?: CameraEntity;
|
@property({ attribute: false }) public stateObj?: CameraEntity;
|
||||||
|
|
||||||
@property({ type: Boolean }) public showControls = false;
|
@property({ type: Boolean, attribute: "controls" })
|
||||||
|
public controls = false;
|
||||||
|
|
||||||
@internalProperty() private _attached = false;
|
@property({ type: Boolean, attribute: "muted" })
|
||||||
|
public muted = false;
|
||||||
|
|
||||||
// We keep track if we should force MJPEG with a string
|
// We keep track if we should force MJPEG with a string
|
||||||
// that way it automatically resets if we change entity.
|
// that way it automatically resets if we change entity.
|
||||||
@internalProperty() private _forceMJPEG: string | undefined = undefined;
|
@internalProperty() private _forceMJPEG?: string;
|
||||||
|
|
||||||
private _hlsPolyfillInstance?: Hls;
|
@internalProperty() private _url?: string;
|
||||||
|
|
||||||
public connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this._attached = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
this._attached = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.stateObj || !this._attached) {
|
if (!this.stateObj) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,51 +56,25 @@ class HaCameraStream extends LitElement {
|
|||||||
)} camera.`}
|
)} camera.`}
|
||||||
/>
|
/>
|
||||||
`
|
`
|
||||||
: html`
|
: this._url
|
||||||
<video
|
? html`
|
||||||
|
<ha-hls-player
|
||||||
autoplay
|
autoplay
|
||||||
muted
|
|
||||||
playsinline
|
playsinline
|
||||||
?controls=${this.showControls}
|
.muted=${this.muted}
|
||||||
@loadeddata=${this._elementResized}
|
.controls=${this.controls}
|
||||||
></video>
|
.hass=${this.hass}
|
||||||
`}
|
.url=${this._url}
|
||||||
|
></ha-hls-player>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
protected updated(changedProps: PropertyValues): void {
|
||||||
super.updated(changedProps);
|
if (changedProps.has("stateObj") && !this._shouldRenderMJPEG) {
|
||||||
|
this._forceMJPEG = undefined;
|
||||||
const stateObjChanged = changedProps.has("stateObj");
|
this._getStreamUrl();
|
||||||
const attachedChanged = changedProps.has("_attached");
|
|
||||||
|
|
||||||
const oldState = changedProps.get("stateObj") as this["stateObj"];
|
|
||||||
const oldEntityId = oldState ? oldState.entity_id : undefined;
|
|
||||||
const curEntityId = this.stateObj ? this.stateObj.entity_id : undefined;
|
|
||||||
|
|
||||||
if (
|
|
||||||
(!stateObjChanged && !attachedChanged) ||
|
|
||||||
(stateObjChanged && oldEntityId === curEntityId)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are no longer attached, destroy polyfill.
|
|
||||||
if (attachedChanged && !this._attached) {
|
|
||||||
this._destroyPolyfill();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nothing to do if we are render MJPEG.
|
|
||||||
if (this._shouldRenderMJPEG) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tear down existing polyfill, if available
|
|
||||||
this._destroyPolyfill();
|
|
||||||
|
|
||||||
if (curEntityId) {
|
|
||||||
this._startHls();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,96 +86,35 @@ class HaCameraStream extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _videoEl(): HTMLVideoElement {
|
private async _getStreamUrl(): Promise<void> {
|
||||||
return this.shadowRoot!.querySelector("video")!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _startHls(): Promise<void> {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const Hls = ((await import(
|
|
||||||
/* webpackChunkName: "hls.js" */ "hls.js"
|
|
||||||
)) as any).default as HLSModule;
|
|
||||||
let hlsSupported = Hls.isSupported();
|
|
||||||
const videoEl = this._videoEl;
|
|
||||||
|
|
||||||
if (!hlsSupported) {
|
|
||||||
hlsSupported =
|
|
||||||
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hlsSupported) {
|
|
||||||
this._forceMJPEG = this.stateObj!.entity_id;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { url } = await fetchStreamUrl(
|
const { url } = await fetchStreamUrl(
|
||||||
this.hass!,
|
this.hass!,
|
||||||
this.stateObj!.entity_id
|
this.stateObj!.entity_id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (Hls.isSupported()) {
|
this._url = url;
|
||||||
this._renderHLSPolyfill(videoEl, Hls, url);
|
|
||||||
} else {
|
|
||||||
this._renderHLSNative(videoEl, url);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Fails if we were unable to get a stream
|
// Fails if we were unable to get a stream
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
this._forceMJPEG = this.stateObj!.entity_id;
|
this._forceMJPEG = this.stateObj!.entity_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
|
|
||||||
videoEl.src = url;
|
|
||||||
await new Promise((resolve) =>
|
|
||||||
videoEl.addEventListener("loadedmetadata", resolve)
|
|
||||||
);
|
|
||||||
videoEl.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _renderHLSPolyfill(
|
|
||||||
videoEl: HTMLVideoElement,
|
|
||||||
// eslint-disable-next-line
|
|
||||||
Hls: HLSModule,
|
|
||||||
url: string
|
|
||||||
) {
|
|
||||||
const hls = new Hls({
|
|
||||||
liveBackBufferLength: 60,
|
|
||||||
fragLoadingTimeOut: 30000,
|
|
||||||
manifestLoadingTimeOut: 30000,
|
|
||||||
levelLoadingTimeOut: 30000,
|
|
||||||
});
|
|
||||||
this._hlsPolyfillInstance = hls;
|
|
||||||
hls.attachMedia(videoEl);
|
|
||||||
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
|
||||||
hls.loadSource(url);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _elementResized() {
|
private _elementResized() {
|
||||||
fireEvent(this, "iron-resize");
|
fireEvent(this, "iron-resize");
|
||||||
}
|
}
|
||||||
|
|
||||||
private _destroyPolyfill(): void {
|
|
||||||
if (this._hlsPolyfillInstance) {
|
|
||||||
this._hlsPolyfillInstance.destroy();
|
|
||||||
this._hlsPolyfillInstance = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResult {
|
static get styles(): CSSResult {
|
||||||
return css`
|
return css`
|
||||||
:host,
|
:host,
|
||||||
img,
|
img {
|
||||||
video {
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
img,
|
img {
|
||||||
video {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
|
|
||||||
@customElement("ha-card")
|
@customElement("ha-card")
|
||||||
class HaCard extends LitElement {
|
export class HaCard extends LitElement {
|
||||||
@property() public header?: string;
|
@property() public header?: string;
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public outlined = false;
|
@property({ type: Boolean, reflect: true }) public outlined = false;
|
||||||
@@ -66,7 +66,7 @@ class HaCard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:host ::slotted(.card-actions) {
|
:host ::slotted(.card-actions) {
|
||||||
border-top: 1px solid #e8e8e8;
|
border-top: 1px solid var(--divider-color, #e8e8e8);
|
||||||
padding: 5px 16px;
|
padding: 5px 16px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
import {
|
|
||||||
LitElement,
|
|
||||||
TemplateResult,
|
|
||||||
property,
|
|
||||||
svg,
|
|
||||||
html,
|
|
||||||
customElement,
|
|
||||||
unsafeCSS,
|
|
||||||
SVGTemplateResult,
|
|
||||||
css,
|
|
||||||
} from "lit-element";
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import progressStyles from "@material/circular-progress/dist/mdc.circular-progress.min.css";
|
import progressStyles from "@material/circular-progress/dist/mdc.circular-progress.min.css";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
svg,
|
||||||
|
SVGTemplateResult,
|
||||||
|
TemplateResult,
|
||||||
|
unsafeCSS,
|
||||||
|
} from "lit-element";
|
||||||
import { classMap } from "lit-html/directives/class-map";
|
import { classMap } from "lit-html/directives/class-map";
|
||||||
|
|
||||||
@customElement("ha-circular-progress")
|
@customElement("ha-circular-progress")
|
||||||
@@ -24,7 +24,7 @@ export class HaCircularProgress extends LitElement {
|
|||||||
@property()
|
@property()
|
||||||
public size: "small" | "medium" | "large" = "medium";
|
public size: "small" | "medium" | "large" = "medium";
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
let indeterminatePart: SVGTemplateResult;
|
let indeterminatePart: SVGTemplateResult;
|
||||||
|
|
||||||
if (this.size === "small") {
|
if (this.size === "small") {
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { Editor } from "codemirror";
|
import { Editor } from "codemirror";
|
||||||
import {
|
import {
|
||||||
customElement,
|
customElement,
|
||||||
property,
|
|
||||||
internalProperty,
|
internalProperty,
|
||||||
|
property,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
UpdatingElement,
|
UpdatingElement,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
@@ -97,15 +97,11 @@ export class HaCodeEditor extends UpdatingElement {
|
|||||||
.CodeMirror {
|
.CodeMirror {
|
||||||
height: var(--code-mirror-height, auto);
|
height: var(--code-mirror-height, auto);
|
||||||
direction: var(--code-mirror-direction, ltr);
|
direction: var(--code-mirror-direction, ltr);
|
||||||
|
font-family: var(--code-font-family, monospace);
|
||||||
}
|
}
|
||||||
.CodeMirror-scroll {
|
.CodeMirror-scroll {
|
||||||
max-height: var(--code-mirror-max-height, --code-mirror-height);
|
max-height: var(--code-mirror-max-height, --code-mirror-height);
|
||||||
}
|
}
|
||||||
.CodeMirror-gutters {
|
|
||||||
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
|
|
||||||
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
|
|
||||||
transition: 0.2s ease border-right;
|
|
||||||
}
|
|
||||||
:host(.error-state) .CodeMirror-gutters {
|
:host(.error-state) .CodeMirror-gutters {
|
||||||
border-color: var(--error-state-color, red);
|
border-color: var(--error-state-color, red);
|
||||||
}
|
}
|
||||||
@@ -113,7 +109,7 @@ export class HaCodeEditor extends UpdatingElement {
|
|||||||
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));
|
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));
|
||||||
}
|
}
|
||||||
.CodeMirror-linenumber {
|
.CodeMirror-linenumber {
|
||||||
color: var(--paper-dialog-color, var(--primary-text-color));
|
color: var(--paper-dialog-color, var(--secondary-text-color));
|
||||||
}
|
}
|
||||||
.rtl .CodeMirror-vscrollbar {
|
.rtl .CodeMirror-vscrollbar {
|
||||||
right: auto;
|
right: auto;
|
||||||
@@ -122,6 +118,100 @@ export class HaCodeEditor extends UpdatingElement {
|
|||||||
.rtl-gutter {
|
.rtl-gutter {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
}
|
}
|
||||||
|
.CodeMirror-gutters {
|
||||||
|
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
|
||||||
|
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
|
||||||
|
transition: 0.2s ease border-right;
|
||||||
|
}
|
||||||
|
.cm-s-default.CodeMirror {
|
||||||
|
background-color: var(--code-editor-background-color, var(--card-background-color));
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
.cm-s-default .CodeMirror-cursor {
|
||||||
|
border-left: 1px solid var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default div.CodeMirror-selected, .cm-s-default.CodeMirror-focused div.CodeMirror-selected {
|
||||||
|
background: rgba(var(--rgb-primary-color), 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .CodeMirror-line::selection,
|
||||||
|
.cm-s-default .CodeMirror-line>span::selection,
|
||||||
|
.cm-s-default .CodeMirror-line>span>span::selection {
|
||||||
|
background: rgba(var(--rgb-primary-color), 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-keyword {
|
||||||
|
color: var(--codemirror-keyword, #6262FF);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-operator {
|
||||||
|
color: var(--codemirror-operator, #cda869);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-variable-2 {
|
||||||
|
color: var(--codemirror-variable-2, #690);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-builtin {
|
||||||
|
color: var(--codemirror-builtin, #9B7536);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-atom {
|
||||||
|
color: var(--codemirror-atom, #F90);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-number {
|
||||||
|
color: var(--codemirror-number, #ca7841);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-def {
|
||||||
|
color: var(--codemirror-def, #8DA6CE);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-string {
|
||||||
|
color: var(--codemirror-string, #07a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-string-2 {
|
||||||
|
color: var(--codemirror-string-2, #bd6b18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-comment {
|
||||||
|
color: var(--codemirror-comment, #777);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-variable {
|
||||||
|
color: var(--codemirror-variable, #07a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-tag {
|
||||||
|
color: var(--codemirror-tag, #997643);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-meta {
|
||||||
|
color: var(--codemirror-meta, #000);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-attribute {
|
||||||
|
color: var(--codemirror-attribute, #d6bb6d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-property {
|
||||||
|
color: var(--codemirror-property, #905);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-qualifier {
|
||||||
|
color: var(--codemirror-qualifier, #690);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-variable-3 {
|
||||||
|
color: var(--codemirror-variable-3, #07a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-type {
|
||||||
|
color: var(--codemirror-type, #07a);
|
||||||
|
}
|
||||||
</style>`;
|
</style>`;
|
||||||
|
|
||||||
this.codemirror = codeMirror(shadowRoot, {
|
this.codemirror = codeMirror(shadowRoot, {
|
||||||
|
@@ -176,6 +176,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
|||||||
this.drawColorWheel();
|
this.drawColorWheel();
|
||||||
this.drawMarker();
|
this.drawMarker();
|
||||||
|
|
||||||
|
if (this.desiredHsColor) {
|
||||||
|
this.setMarkerOnColor(this.desiredHsColor);
|
||||||
|
this.applyColorToCanvas(this.desiredHsColor);
|
||||||
|
}
|
||||||
|
|
||||||
this.interactionLayer.addEventListener("mousedown", (ev) =>
|
this.interactionLayer.addEventListener("mousedown", (ev) =>
|
||||||
this.onMouseDown(ev)
|
this.onMouseDown(ev)
|
||||||
);
|
);
|
||||||
@@ -188,10 +193,10 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
|||||||
// origin is wheel center
|
// origin is wheel center
|
||||||
// returns {x: X, y: Y} object
|
// returns {x: X, y: Y} object
|
||||||
convertToCanvasCoordinates(clientX, clientY) {
|
convertToCanvasCoordinates(clientX, clientY) {
|
||||||
var svgPoint = this.interactionLayer.createSVGPoint();
|
const svgPoint = this.interactionLayer.createSVGPoint();
|
||||||
svgPoint.x = clientX;
|
svgPoint.x = clientX;
|
||||||
svgPoint.y = clientY;
|
svgPoint.y = clientY;
|
||||||
var cc = svgPoint.matrixTransform(
|
const cc = svgPoint.matrixTransform(
|
||||||
this.interactionLayer.getScreenCTM().inverse()
|
this.interactionLayer.getScreenCTM().inverse()
|
||||||
);
|
);
|
||||||
return { x: cc.x, y: cc.y };
|
return { x: cc.x, y: cc.y };
|
||||||
@@ -225,7 +230,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
|||||||
// Touch events
|
// Touch events
|
||||||
|
|
||||||
onTouchStart(ev) {
|
onTouchStart(ev) {
|
||||||
var touch = ev.changedTouches[0];
|
const touch = ev.changedTouches[0];
|
||||||
const cc = this.convertToCanvasCoordinates(touch.clientX, touch.clientY);
|
const cc = this.convertToCanvasCoordinates(touch.clientX, touch.clientY);
|
||||||
// return if we're not on the wheel
|
// return if we're not on the wheel
|
||||||
if (!this.isInWheel(cc.x, cc.y)) {
|
if (!this.isInWheel(cc.x, cc.y)) {
|
||||||
@@ -275,8 +280,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
|||||||
|
|
||||||
// Process user input to color
|
// Process user input to color
|
||||||
processUserSelect(ev) {
|
processUserSelect(ev) {
|
||||||
var canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
|
const canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
|
||||||
var hs = this.getColor(canvasXY.x, canvasXY.y);
|
const hs = this.getColor(canvasXY.x, canvasXY.y);
|
||||||
this.onColorSelect(hs);
|
this.onColorSelect(hs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,17 +324,23 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
|||||||
|
|
||||||
// set marker position to the given color
|
// set marker position to the given color
|
||||||
setMarkerOnColor(hs) {
|
setMarkerOnColor(hs) {
|
||||||
var dist = hs.s * this.radius;
|
if (!this.marker || !this.tooltip) {
|
||||||
var theta = ((hs.h - 180) / 180) * Math.PI;
|
return;
|
||||||
var markerdX = -dist * Math.cos(theta);
|
}
|
||||||
var markerdY = -dist * Math.sin(theta);
|
const dist = hs.s * this.radius;
|
||||||
var translateString = `translate(${markerdX},${markerdY})`;
|
const theta = ((hs.h - 180) / 180) * Math.PI;
|
||||||
|
const markerdX = -dist * Math.cos(theta);
|
||||||
|
const markerdY = -dist * Math.sin(theta);
|
||||||
|
const translateString = `translate(${markerdX},${markerdY})`;
|
||||||
this.marker.setAttribute("transform", translateString);
|
this.marker.setAttribute("transform", translateString);
|
||||||
this.tooltip.setAttribute("transform", translateString);
|
this.tooltip.setAttribute("transform", translateString);
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply given color to interface elements
|
// apply given color to interface elements
|
||||||
applyColorToCanvas(hs) {
|
applyColorToCanvas(hs) {
|
||||||
|
if (!this.interactionLayer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// we're not really converting hs to hsl here, but we keep it cheap
|
// we're not really converting hs to hsl here, but we keep it cheap
|
||||||
// setting the color on the interactionLayer, the svg elements can inherit
|
// setting the color on the interactionLayer, the svg elements can inherit
|
||||||
this.interactionLayer.style.color = `hsl(${hs.h}, 100%, ${
|
this.interactionLayer.style.color = `hsl(${hs.h}, 100%, ${
|
||||||
@@ -358,8 +369,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
|||||||
|
|
||||||
// get angle (degrees)
|
// get angle (degrees)
|
||||||
getAngle(dX, dY) {
|
getAngle(dX, dY) {
|
||||||
var theta = Math.atan2(-dY, -dX); // radians from the left edge, clockwise = positive
|
const theta = Math.atan2(-dY, -dX); // radians from the left edge, clockwise = positive
|
||||||
var angle = (theta / Math.PI) * 180 + 180; // degrees, clockwise from right
|
const angle = (theta / Math.PI) * 180 + 180; // degrees, clockwise from right
|
||||||
return angle;
|
return angle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,9 +389,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
getColor(x, y) {
|
getColor(x, y) {
|
||||||
var hue = this.getAngle(x, y); // degrees, clockwise from right
|
const hue = this.getAngle(x, y); // degrees, clockwise from right
|
||||||
var relativeDistance = this.getDistance(x, y); // edge of radius = 1
|
const relativeDistance = this.getDistance(x, y); // edge of radius = 1
|
||||||
var sat = Math.min(relativeDistance, 1); // Distance from center
|
const sat = Math.min(relativeDistance, 1); // Distance from center
|
||||||
return { h: hue, s: sat };
|
return { h: hue, s: sat };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,9 +413,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
|||||||
if (this.saturationSegments === 1) {
|
if (this.saturationSegments === 1) {
|
||||||
hs.s = 1;
|
hs.s = 1;
|
||||||
} else {
|
} else {
|
||||||
var segmentSize = 1 / this.saturationSegments;
|
const segmentSize = 1 / this.saturationSegments;
|
||||||
var saturationStep = 1 / (this.saturationSegments - 1);
|
const saturationStep = 1 / (this.saturationSegments - 1);
|
||||||
var calculatedSat = Math.floor(hs.s / segmentSize) * saturationStep;
|
const calculatedSat = Math.floor(hs.s / segmentSize) * saturationStep;
|
||||||
hs.s = Math.min(calculatedSat, 1);
|
hs.s = Math.min(calculatedSat, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -477,9 +488,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
|||||||
hueSegments = hueSegments || 360; // reset 0 segments to 360
|
hueSegments = hueSegments || 360; // reset 0 segments to 360
|
||||||
const angleStep = 360 / hueSegments;
|
const angleStep = 360 / hueSegments;
|
||||||
const halfAngleStep = angleStep / 2; // center segments on color
|
const halfAngleStep = angleStep / 2; // center segments on color
|
||||||
for (var angle = 0; angle <= 360; angle += angleStep) {
|
for (let angle = 0; angle <= 360; angle += angleStep) {
|
||||||
var startAngle = (angle - halfAngleStep) * (Math.PI / 180);
|
const startAngle = (angle - halfAngleStep) * (Math.PI / 180);
|
||||||
var endAngle = (angle + halfAngleStep + 1) * (Math.PI / 180);
|
const endAngle = (angle + halfAngleStep + 1) * (Math.PI / 180);
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
context.moveTo(cX, cY);
|
context.moveTo(cX, cY);
|
||||||
context.arc(
|
context.arc(
|
||||||
@@ -492,7 +503,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
|||||||
);
|
);
|
||||||
context.closePath();
|
context.closePath();
|
||||||
// gradient
|
// gradient
|
||||||
var gradient = context.createRadialGradient(
|
const gradient = context.createRadialGradient(
|
||||||
cX,
|
cX,
|
||||||
cY,
|
cY,
|
||||||
0,
|
0,
|
||||||
@@ -507,8 +518,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
|||||||
if (saturationSegments > 0) {
|
if (saturationSegments > 0) {
|
||||||
const ratioStep = 1 / saturationSegments;
|
const ratioStep = 1 / saturationSegments;
|
||||||
let ratio = 0;
|
let ratio = 0;
|
||||||
for (var stop = 1; stop < saturationSegments; stop += 1) {
|
for (let stop = 1; stop < saturationSegments; stop += 1) {
|
||||||
var prevLighness = lightness;
|
const prevLighness = lightness;
|
||||||
ratio = stop * ratioStep;
|
ratio = stop * ratioStep;
|
||||||
lightness = 100 - 50 * ratio;
|
lightness = 100 - 50 * ratio;
|
||||||
gradient.addColorStop(
|
gradient.addColorStop(
|
||||||
|
@@ -95,7 +95,7 @@ class HaCoverControls extends PolymerElement {
|
|||||||
if (stateObj.state === UNAVAILABLE) {
|
if (stateObj.state === UNAVAILABLE) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
var assumedState = stateObj.attributes.assumed_state === true;
|
const assumedState = stateObj.attributes.assumed_state === true;
|
||||||
return (entityObj.isFullyOpen || entityObj.isOpening) && !assumedState;
|
return (entityObj.isFullyOpen || entityObj.isOpening) && !assumedState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ class HaCoverControls extends PolymerElement {
|
|||||||
if (stateObj.state === UNAVAILABLE) {
|
if (stateObj.state === UNAVAILABLE) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
var assumedState = stateObj.attributes.assumed_state === true;
|
const assumedState = stateObj.attributes.assumed_state === true;
|
||||||
return (entityObj.isFullyClosed || entityObj.isClosing) && !assumedState;
|
return (entityObj.isFullyClosed || entityObj.isClosing) && !assumedState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -75,7 +75,7 @@ class HaCoverTiltControls extends PolymerElement {
|
|||||||
if (stateObj.state === UNAVAILABLE) {
|
if (stateObj.state === UNAVAILABLE) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
var assumedState = stateObj.attributes.assumed_state === true;
|
const assumedState = stateObj.attributes.assumed_state === true;
|
||||||
return entityObj.isFullyOpenTilt && !assumedState;
|
return entityObj.isFullyOpenTilt && !assumedState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ class HaCoverTiltControls extends PolymerElement {
|
|||||||
if (stateObj.state === UNAVAILABLE) {
|
if (stateObj.state === UNAVAILABLE) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
var assumedState = stateObj.attributes.assumed_state === true;
|
const assumedState = stateObj.attributes.assumed_state === true;
|
||||||
return entityObj.isFullyClosedTilt && !assumedState;
|
return entityObj.isFullyClosedTilt && !assumedState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -17,6 +17,8 @@ import "./ha-svg-icon";
|
|||||||
import "@polymer/paper-input/paper-input";
|
import "@polymer/paper-input/paper-input";
|
||||||
import "@material/mwc-list/mwc-list";
|
import "@material/mwc-list/mwc-list";
|
||||||
import "./date-range-picker";
|
import "./date-range-picker";
|
||||||
|
import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||||
|
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||||
|
|
||||||
export interface DateRangePickerRanges {
|
export interface DateRangePickerRanges {
|
||||||
[key: string]: [Date, Date];
|
[key: string]: [Date, Date];
|
||||||
@@ -36,11 +38,14 @@ export class HaDateRangePicker extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) private _hour24format = false;
|
@property({ type: Boolean }) private _hour24format = false;
|
||||||
|
|
||||||
|
@property({ type: String }) private _rtlDirection = "ltr";
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
protected updated(changedProps: PropertyValues) {
|
||||||
if (changedProps.has("hass")) {
|
if (changedProps.has("hass")) {
|
||||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
if (!oldHass || oldHass.language !== this.hass.language) {
|
if (!oldHass || oldHass.language !== this.hass.language) {
|
||||||
this._hour24format = this._compute24hourFormat();
|
this._hour24format = this._compute24hourFormat();
|
||||||
|
this._rtlDirection = computeRTLDirection(this.hass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,16 +81,14 @@ export class HaDateRangePicker extends LitElement {
|
|||||||
></paper-input>
|
></paper-input>
|
||||||
</div>
|
</div>
|
||||||
${this.ranges
|
${this.ranges
|
||||||
? html`<div slot="ranges" class="date-range-ranges">
|
? html`<div
|
||||||
<mwc-list @click=${this._setDateRange}>
|
slot="ranges"
|
||||||
${Object.entries(this.ranges).map(
|
class="date-range-ranges"
|
||||||
([name, dates]) => html`<mwc-list-item
|
.dir=${this._rtlDirection}
|
||||||
.activated=${this.startDate.getTime() ===
|
>
|
||||||
dates[0].getTime() &&
|
<mwc-list @action=${this._setDateRange} activatable>
|
||||||
this.endDate.getTime() === dates[1].getTime()}
|
${Object.keys(this.ranges).map(
|
||||||
.startDate=${dates[0]}
|
(name) => html`<mwc-list-item>
|
||||||
.endDate=${dates[1]}
|
|
||||||
>
|
|
||||||
${name}
|
${name}
|
||||||
</mwc-list-item>`
|
</mwc-list-item>`
|
||||||
)}
|
)}
|
||||||
@@ -116,12 +119,10 @@ export class HaDateRangePicker extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setDateRange(ev: Event) {
|
private _setDateRange(ev: CustomEvent<ActionDetail>) {
|
||||||
const target = ev.target as any;
|
const dateRange = Object.values(this.ranges!)[ev.detail.index];
|
||||||
const startDate = target.startDate;
|
|
||||||
const endDate = target.endDate;
|
|
||||||
const dateRangePicker = this._dateRangePicker;
|
const dateRangePicker = this._dateRangePicker;
|
||||||
dateRangePicker.clickRange([startDate, endDate]);
|
dateRangePicker.clickRange(dateRange);
|
||||||
dateRangePicker.clickedApply();
|
dateRangePicker.clickedApply();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,13 +163,6 @@ export class HaDateRangePicker extends LitElement {
|
|||||||
border-right: 1px solid var(--divider-color);
|
border-right: 1px solid var(--divider-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 800px) {
|
|
||||||
.date-range-ranges {
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: 1px solid var(--divider-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-range-footer {
|
.date-range-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -178,12 +172,30 @@ export class HaDateRangePicker extends LitElement {
|
|||||||
|
|
||||||
paper-input {
|
paper-input {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 200px;
|
max-width: 250px;
|
||||||
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
paper-input:last-child {
|
paper-input:last-child {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 800px) {
|
||||||
|
.date-range-ranges {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 500px) {
|
||||||
|
paper-input {
|
||||||
|
min-width: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-svg-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,21 +1,21 @@
|
|||||||
import "@material/mwc-dialog";
|
import "@material/mwc-dialog";
|
||||||
import type { Dialog } from "@material/mwc-dialog";
|
import type { Dialog } from "@material/mwc-dialog";
|
||||||
import { style } from "@material/mwc-dialog/mwc-dialog-css";
|
import { style } from "@material/mwc-dialog/mwc-dialog-css";
|
||||||
import "./ha-icon-button";
|
|
||||||
import { css, CSSResult, customElement, html } from "lit-element";
|
|
||||||
import type { Constructor, HomeAssistant } from "../types";
|
|
||||||
import { mdiClose } from "@mdi/js";
|
import { mdiClose } from "@mdi/js";
|
||||||
import { computeRTL } from "../common/util/compute_rtl";
|
import { css, CSSResult, customElement, html } from "lit-element";
|
||||||
|
import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||||
|
import type { Constructor, HomeAssistant } from "../types";
|
||||||
|
import "./ha-icon-button";
|
||||||
|
|
||||||
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
|
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
|
||||||
|
|
||||||
export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
|
export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
|
||||||
${title}
|
<span class="header_title">${title}</span>
|
||||||
<mwc-icon-button
|
<mwc-icon-button
|
||||||
aria-label=${hass.localize("ui.dialogs.generic.close")}
|
aria-label=${hass.localize("ui.dialogs.generic.close")}
|
||||||
dialogAction="close"
|
dialogAction="close"
|
||||||
?rtl=${computeRTL(hass)}
|
|
||||||
class="header_button"
|
class="header_button"
|
||||||
|
dir=${computeRTLDirection(hass)}
|
||||||
>
|
>
|
||||||
<ha-svg-icon path=${mdiClose}></ha-svg-icon>
|
<ha-svg-icon path=${mdiClose}></ha-svg-icon>
|
||||||
</mwc-icon-button>
|
</mwc-icon-button>
|
||||||
@@ -23,6 +23,10 @@ export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
|
|||||||
|
|
||||||
@customElement("ha-dialog")
|
@customElement("ha-dialog")
|
||||||
export class HaDialog extends MwcDialog {
|
export class HaDialog extends MwcDialog {
|
||||||
|
public scrollToPos(x: number, y: number) {
|
||||||
|
this.contentElement.scrollTo(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
protected renderHeading() {
|
protected renderHeading() {
|
||||||
return html`<slot name="heading">
|
return html`<slot name="heading">
|
||||||
${super.renderHeading()}
|
${super.renderHeading()}
|
||||||
@@ -34,10 +38,12 @@ export class HaDialog extends MwcDialog {
|
|||||||
style,
|
style,
|
||||||
css`
|
css`
|
||||||
.mdc-dialog {
|
.mdc-dialog {
|
||||||
|
--mdc-dialog-scroll-divider-color: var(--divider-color);
|
||||||
z-index: var(--dialog-z-index, 7);
|
z-index: var(--dialog-z-index, 7);
|
||||||
}
|
}
|
||||||
.mdc-dialog__actions {
|
.mdc-dialog__actions {
|
||||||
justify-content: var(--justify-action-buttons, flex-end);
|
justify-content: var(--justify-action-buttons, flex-end);
|
||||||
|
padding-bottom: max(env(safe-area-inset-bottom), 8px);
|
||||||
}
|
}
|
||||||
.mdc-dialog__container {
|
.mdc-dialog__container {
|
||||||
align-items: var(--vertial-align-dialog, center);
|
align-items: var(--vertial-align-dialog, center);
|
||||||
@@ -50,10 +56,21 @@ export class HaDialog extends MwcDialog {
|
|||||||
position: var(--dialog-content-position, relative);
|
position: var(--dialog-content-position, relative);
|
||||||
padding: var(--dialog-content-padding, 20px 24px);
|
padding: var(--dialog-content-padding, 20px 24px);
|
||||||
}
|
}
|
||||||
|
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
|
||||||
|
padding-bottom: max(
|
||||||
|
var(--dialog-content-padding, 20px),
|
||||||
|
env(safe-area-inset-bottom)
|
||||||
|
);
|
||||||
|
}
|
||||||
.mdc-dialog .mdc-dialog__surface {
|
.mdc-dialog .mdc-dialog__surface {
|
||||||
position: var(--dialog-surface-position, relative);
|
position: var(--dialog-surface-position, relative);
|
||||||
|
top: var(--dialog-surface-top);
|
||||||
min-height: var(--mdc-dialog-min-height, auto);
|
min-height: var(--mdc-dialog-min-height, auto);
|
||||||
}
|
}
|
||||||
|
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
.header_button {
|
.header_button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
@@ -61,10 +78,17 @@ export class HaDialog extends MwcDialog {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
mwc-icon-button[rtl].header_button {
|
.header_title {
|
||||||
|
margin-right: 40px;
|
||||||
|
}
|
||||||
|
[dir="rtl"].header_button {
|
||||||
right: auto;
|
right: auto;
|
||||||
left: 16px;
|
left: 16px;
|
||||||
}
|
}
|
||||||
|
[dir="rtl"].header_title {
|
||||||
|
margin-left: 40px;
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -54,7 +54,8 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
|||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
<ha-paper-slider
|
<ha-paper-slider
|
||||||
pin=""
|
pin
|
||||||
|
editable
|
||||||
.value=${this._value}
|
.value=${this._value}
|
||||||
.min=${this.schema.valueMin}
|
.min=${this.schema.valueMin}
|
||||||
.max=${this.schema.valueMax}
|
.max=${this.schema.valueMax}
|
||||||
@@ -111,6 +112,10 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
|||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
ha-paper-slider {
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
|
|
||||||
import "@polymer/paper-item/paper-item";
|
import "@polymer/paper-item/paper-item";
|
||||||
import "@polymer/paper-listbox/paper-listbox";
|
import "@polymer/paper-listbox/paper-listbox";
|
||||||
import {
|
import {
|
||||||
@@ -12,6 +11,7 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import "../ha-paper-dropdown-menu";
|
||||||
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form";
|
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form";
|
||||||
|
|
||||||
@customElement("ha-form-select")
|
@customElement("ha-form-select")
|
||||||
@@ -24,7 +24,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
|||||||
|
|
||||||
@property() public suffix!: string;
|
@property() public suffix!: string;
|
||||||
|
|
||||||
@query("paper-dropdown-menu") private _input?: HTMLElement;
|
@query("ha-paper-dropdown-menu") private _input?: HTMLElement;
|
||||||
|
|
||||||
public focus() {
|
public focus() {
|
||||||
if (this._input) {
|
if (this._input) {
|
||||||
@@ -34,7 +34,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
|||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<paper-dropdown-menu .label=${this.label}>
|
<ha-paper-dropdown-menu .label=${this.label}>
|
||||||
<paper-listbox
|
<paper-listbox
|
||||||
slot="dropdown-content"
|
slot="dropdown-content"
|
||||||
attr-for-selected="item-value"
|
attr-for-selected="item-value"
|
||||||
@@ -51,7 +51,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
|||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</paper-listbox>
|
</paper-listbox>
|
||||||
</paper-dropdown-menu>
|
</ha-paper-dropdown-menu>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
|||||||
|
|
||||||
static get styles(): CSSResult {
|
static get styles(): CSSResult {
|
||||||
return css`
|
return css`
|
||||||
paper-dropdown-menu {
|
ha-paper-dropdown-menu {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@@ -100,7 +100,7 @@ export interface HaFormTimeData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface HaFormElement extends LitElement {
|
export interface HaFormElement extends LitElement {
|
||||||
schema: HaFormSchema;
|
schema: HaFormSchema | HaFormSchema[];
|
||||||
data?: HaFormDataContainer | HaFormData;
|
data?: HaFormDataContainer | HaFormData;
|
||||||
label?: string;
|
label?: string;
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
@@ -110,7 +110,7 @@ export interface HaFormElement extends LitElement {
|
|||||||
export class HaForm extends LitElement implements HaFormElement {
|
export class HaForm extends LitElement implements HaFormElement {
|
||||||
@property() public data!: HaFormDataContainer | HaFormData;
|
@property() public data!: HaFormDataContainer | HaFormData;
|
||||||
|
|
||||||
@property() public schema!: HaFormSchema;
|
@property() public schema!: HaFormSchema | HaFormSchema[];
|
||||||
|
|
||||||
@property() public error;
|
@property() public error;
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
|||||||
: "";
|
: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private _computeError(error, schema: HaFormSchema) {
|
private _computeError(error, schema: HaFormSchema | HaFormSchema[]) {
|
||||||
return this.computeError ? this.computeError(error, schema) : error;
|
return this.computeError ? this.computeError(error, schema) : error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
|||||||
|
|
||||||
private _valueChanged(ev: CustomEvent) {
|
private _valueChanged(ev: CustomEvent) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const schema = (ev.target as HaFormElement).schema;
|
const schema = (ev.target as HaFormElement).schema as HaFormSchema;
|
||||||
const data = this.data as HaFormDataContainer;
|
const data = this.data as HaFormDataContainer;
|
||||||
data[schema.name] = ev.detail.value;
|
data[schema.name] = ev.detail.value;
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
|
143
src/components/ha-gauge.ts
Normal file
143
src/components/ha-gauge.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import {
|
||||||
|
LitElement,
|
||||||
|
svg,
|
||||||
|
customElement,
|
||||||
|
css,
|
||||||
|
property,
|
||||||
|
internalProperty,
|
||||||
|
PropertyValues,
|
||||||
|
} from "lit-element";
|
||||||
|
import { styleMap } from "lit-html/directives/style-map";
|
||||||
|
import { afterNextRender } from "../common/util/render-status";
|
||||||
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
|
import { getValueInPercentage, normalize } from "../util/calculate";
|
||||||
|
|
||||||
|
const getAngle = (value: number, min: number, max: number) => {
|
||||||
|
const percentage = getValueInPercentage(normalize(value, min, max), min, max);
|
||||||
|
return (percentage * 180) / 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Workaround for https://github.com/home-assistant/frontend/issues/6467
|
||||||
|
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
|
|
||||||
|
@customElement("ha-gauge")
|
||||||
|
export class Gauge extends LitElement {
|
||||||
|
@property({ type: Number }) public min = 0;
|
||||||
|
|
||||||
|
@property({ type: Number }) public max = 100;
|
||||||
|
|
||||||
|
@property({ type: Number }) public value = 0;
|
||||||
|
|
||||||
|
@property() public label = "";
|
||||||
|
|
||||||
|
@internalProperty() private _angle = 0;
|
||||||
|
|
||||||
|
@internalProperty() private _updated = false;
|
||||||
|
|
||||||
|
protected firstUpdated(changedProperties: PropertyValues) {
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
|
// Wait for the first render for the initial animation to work
|
||||||
|
afterNextRender(() => {
|
||||||
|
this._updated = true;
|
||||||
|
this._angle = getAngle(this.value, this.min, this.max);
|
||||||
|
this._rescale_svg();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProperties: PropertyValues) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
if (!this._updated || !changedProperties.has("value")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._angle = getAngle(this.value, this.min, this.max);
|
||||||
|
this._rescale_svg();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return svg`
|
||||||
|
<svg viewBox="0 0 100 50" class="gauge">
|
||||||
|
<path
|
||||||
|
class="dial"
|
||||||
|
d="M 10 50 A 40 40 0 0 1 90 50"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="value"
|
||||||
|
d="M 90 50.001 A 40 40 0 0 1 10 50"
|
||||||
|
style=${ifDefined(
|
||||||
|
!isSafari
|
||||||
|
? styleMap({ transform: `rotate(${this._angle}deg)` })
|
||||||
|
: undefined
|
||||||
|
)}
|
||||||
|
transform=${ifDefined(
|
||||||
|
isSafari ? `rotate(${this._angle} 50 50)` : undefined
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
${
|
||||||
|
isSafari
|
||||||
|
? svg`<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="rotate"
|
||||||
|
from="0 50 50"
|
||||||
|
to="${this._angle} 50 50"
|
||||||
|
dur="1s"
|
||||||
|
/>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
<svg class="text">
|
||||||
|
<text class="value-text">
|
||||||
|
${this.value} ${this.label}
|
||||||
|
</text>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _rescale_svg() {
|
||||||
|
// Set the viewbox of the SVG containing the value to perfectly
|
||||||
|
// fit the text
|
||||||
|
// That way it will auto-scale correctly
|
||||||
|
const svgRoot = this.shadowRoot!.querySelector(".text")!;
|
||||||
|
const box = svgRoot.querySelector("text")!.getBBox()!;
|
||||||
|
svgRoot.setAttribute(
|
||||||
|
"viewBox",
|
||||||
|
`${box.x} ${box!.y} ${box.width} ${box.height}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.dial {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--primary-background-color);
|
||||||
|
stroke-width: 15;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 15;
|
||||||
|
stroke: var(--gauge-color);
|
||||||
|
transform-origin: 50% 100%;
|
||||||
|
transition: all 1s ease 0s;
|
||||||
|
}
|
||||||
|
.gauge {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
position: absolute;
|
||||||
|
max-height: 40%;
|
||||||
|
max-width: 55%;
|
||||||
|
left: 50%;
|
||||||
|
bottom: -6%;
|
||||||
|
transform: translate(-50%, 0%);
|
||||||
|
}
|
||||||
|
.value-text {
|
||||||
|
font-size: 50px;
|
||||||
|
fill: var(--primary-text-color);
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
import { customElement, LitElement, html, unsafeCSS, css } from "lit-element";
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import topAppBarStyles from "@material/top-app-bar/dist/mdc.top-app-bar.min.css";
|
import topAppBarStyles from "@material/top-app-bar/dist/mdc.top-app-bar.min.css";
|
||||||
|
import { css, customElement, html, LitElement, unsafeCSS } from "lit-element";
|
||||||
|
|
||||||
@customElement("ha-header-bar")
|
@customElement("ha-header-bar")
|
||||||
export class HaHeaderBar extends LitElement {
|
export class HaHeaderBar extends LitElement {
|
||||||
|
216
src/components/ha-hls-player.ts
Normal file
216
src/components/ha-hls-player.ts
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
internalProperty,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
PropertyValues,
|
||||||
|
query,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { nextRender } from "../common/util/render-status";
|
||||||
|
import { getExternalConfig } from "../external_app/external_config";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
type HLSModule = typeof import("hls.js");
|
||||||
|
|
||||||
|
@customElement("ha-hls-player")
|
||||||
|
class HaHLSPlayer extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public url!: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "controls" })
|
||||||
|
public controls = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "muted" })
|
||||||
|
public muted = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "autoplay" })
|
||||||
|
public autoPlay = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "playsinline" })
|
||||||
|
public playsInline = false;
|
||||||
|
|
||||||
|
@query("video") private _videoEl!: HTMLVideoElement;
|
||||||
|
|
||||||
|
@internalProperty() private _attached = false;
|
||||||
|
|
||||||
|
private _hlsPolyfillInstance?: Hls;
|
||||||
|
|
||||||
|
private _useExoPlayer = false;
|
||||||
|
|
||||||
|
public connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._attached = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._attached = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this._attached) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<video
|
||||||
|
?autoplay=${this.autoPlay}
|
||||||
|
.muted=${this.muted}
|
||||||
|
?playsinline=${this.playsInline}
|
||||||
|
?controls=${this.controls}
|
||||||
|
@loadeddata=${this._elementResized}
|
||||||
|
></video>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
super.updated(changedProps);
|
||||||
|
|
||||||
|
const attachedChanged = changedProps.has("_attached");
|
||||||
|
const urlChanged = changedProps.has("url");
|
||||||
|
|
||||||
|
if (!urlChanged && !attachedChanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are no longer attached, destroy polyfill
|
||||||
|
if (attachedChanged && !this._attached) {
|
||||||
|
// Tear down existing polyfill, if available
|
||||||
|
this._destroyPolyfill();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._destroyPolyfill();
|
||||||
|
this._startHls();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getUseExoPlayer(): Promise<boolean> {
|
||||||
|
if (!this.hass!.auth.external) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const externalConfig = await getExternalConfig(this.hass!.auth.external);
|
||||||
|
return externalConfig && externalConfig.hasExoPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _startHls(): Promise<void> {
|
||||||
|
let hls: any;
|
||||||
|
const videoEl = this._videoEl;
|
||||||
|
this._useExoPlayer = await this._getUseExoPlayer();
|
||||||
|
if (!this._useExoPlayer) {
|
||||||
|
hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
|
||||||
|
.default as HLSModule;
|
||||||
|
let hlsSupported = hls.isSupported();
|
||||||
|
|
||||||
|
if (!hlsSupported) {
|
||||||
|
hlsSupported =
|
||||||
|
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hlsSupported) {
|
||||||
|
this._videoEl.innerHTML = this.hass.localize(
|
||||||
|
"ui.components.media-browser.video_not_supported"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = this.url;
|
||||||
|
|
||||||
|
if (this._useExoPlayer) {
|
||||||
|
this._renderHLSExoPlayer(url);
|
||||||
|
} else if (hls.isSupported()) {
|
||||||
|
this._renderHLSPolyfill(videoEl, hls, url);
|
||||||
|
} else {
|
||||||
|
this._renderHLSNative(videoEl, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _renderHLSExoPlayer(url: string) {
|
||||||
|
window.addEventListener("resize", this._resizeExoPlayer);
|
||||||
|
this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer);
|
||||||
|
this._videoEl.style.visibility = "hidden";
|
||||||
|
await this.hass!.auth.external!.sendMessage({
|
||||||
|
type: "exoplayer/play_hls",
|
||||||
|
payload: new URL(url, window.location.href).toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _resizeExoPlayer = () => {
|
||||||
|
const rect = this._videoEl.getBoundingClientRect();
|
||||||
|
this.hass!.auth.external!.fireMessage({
|
||||||
|
type: "exoplayer/resize",
|
||||||
|
payload: {
|
||||||
|
left: rect.left,
|
||||||
|
top: rect.top,
|
||||||
|
right: rect.right,
|
||||||
|
bottom: rect.bottom,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private async _renderHLSPolyfill(
|
||||||
|
videoEl: HTMLVideoElement,
|
||||||
|
Hls: HLSModule,
|
||||||
|
url: string
|
||||||
|
) {
|
||||||
|
const hls = new Hls({
|
||||||
|
liveBackBufferLength: 60,
|
||||||
|
fragLoadingTimeOut: 30000,
|
||||||
|
manifestLoadingTimeOut: 30000,
|
||||||
|
levelLoadingTimeOut: 30000,
|
||||||
|
});
|
||||||
|
this._hlsPolyfillInstance = hls;
|
||||||
|
hls.attachMedia(videoEl);
|
||||||
|
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||||
|
hls.loadSource(url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
|
||||||
|
videoEl.src = url;
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
videoEl.addEventListener("loadedmetadata", resolve)
|
||||||
|
);
|
||||||
|
videoEl.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _elementResized() {
|
||||||
|
fireEvent(this, "iron-resize");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _destroyPolyfill() {
|
||||||
|
if (this._hlsPolyfillInstance) {
|
||||||
|
this._hlsPolyfillInstance.destroy();
|
||||||
|
this._hlsPolyfillInstance = undefined;
|
||||||
|
}
|
||||||
|
if (this._useExoPlayer) {
|
||||||
|
window.removeEventListener("resize", this._resizeExoPlayer);
|
||||||
|
this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
:host,
|
||||||
|
video {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-hls-player": HaHLSPlayer;
|
||||||
|
}
|
||||||
|
}
|
@@ -89,7 +89,7 @@ const mdiRenameMapping = {
|
|||||||
"library-movie": "filmstrip-box-multiple",
|
"library-movie": "filmstrip-box-multiple",
|
||||||
"library-music": "music-box-multiple",
|
"library-music": "music-box-multiple",
|
||||||
"library-music-outline": "music-box-multiple-outline",
|
"library-music-outline": "music-box-multiple-outline",
|
||||||
"library-video": "play-box-mutiple",
|
"library-video": "play-box-multiple",
|
||||||
markdown: "language-markdown",
|
markdown: "language-markdown",
|
||||||
"markdown-outline": "language-markdown-outline",
|
"markdown-outline": "language-markdown-outline",
|
||||||
"message-settings-variant": "message-cog",
|
"message-settings-variant": "message-cog",
|
||||||
@@ -106,6 +106,7 @@ const mdiRenameMapping = {
|
|||||||
pot: "pot-steam",
|
pot: "pot-steam",
|
||||||
ruby: "language-ruby",
|
ruby: "language-ruby",
|
||||||
sailing: "sail-boat",
|
sailing: "sail-boat",
|
||||||
|
scooter: "human-scooter",
|
||||||
settings: "cog",
|
settings: "cog",
|
||||||
"settings-box": "cog-box",
|
"settings-box": "cog-box",
|
||||||
"settings-outline": "cog-outline",
|
"settings-outline": "cog-outline",
|
||||||
@@ -151,6 +152,8 @@ const mdiRenameMapping = {
|
|||||||
"textbox-lock": "form-textbox-lock",
|
"textbox-lock": "form-textbox-lock",
|
||||||
"textbox-password": "form-textbox-password",
|
"textbox-password": "form-textbox-password",
|
||||||
"syllabary-katakana-half-width": "syllabary-katakana-halfwidth",
|
"syllabary-katakana-half-width": "syllabary-katakana-halfwidth",
|
||||||
|
"visual-studio-code": "microsoft-visual-studio-code",
|
||||||
|
"visual-studio": "microsoft-visual-studio",
|
||||||
};
|
};
|
||||||
|
|
||||||
const mdiRemovedIcons = new Set([
|
const mdiRemovedIcons = new Set([
|
||||||
@@ -191,6 +194,7 @@ const mdiRemovedIcons = new Set([
|
|||||||
"medium",
|
"medium",
|
||||||
"meetup",
|
"meetup",
|
||||||
"mixcloud",
|
"mixcloud",
|
||||||
|
"mixer",
|
||||||
"nfc-off",
|
"nfc-off",
|
||||||
"npm-variant",
|
"npm-variant",
|
||||||
"npm-variant-outline",
|
"npm-variant-outline",
|
||||||
|
@@ -23,7 +23,6 @@ class HaMarkdownElement extends UpdatingElement {
|
|||||||
{
|
{
|
||||||
breaks: this.breaks,
|
breaks: this.breaks,
|
||||||
gfm: true,
|
gfm: true,
|
||||||
tables: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
allowSvg: this.allowSvg,
|
allowSvg: this.allowSvg,
|
||||||
|
@@ -57,6 +57,10 @@ class HaMarkdown extends LitElement {
|
|||||||
background-color: var(--markdown-code-background-color, none);
|
background-color: var(--markdown-code-background-color, none);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
ha-markdown-element svg {
|
||||||
|
background-color: var(--markdown-svg-background-color, none);
|
||||||
|
color: var(--markdown-svg-color, none);
|
||||||
|
}
|
||||||
ha-markdown-element code {
|
ha-markdown-element code {
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
padding: 0.2em 0.4em;
|
padding: 0.2em 0.4em;
|
||||||
@@ -70,8 +74,8 @@ class HaMarkdown extends LitElement {
|
|||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
ha-markdown-element h2 {
|
ha-markdown-element h2 {
|
||||||
font-size: 1.5em !important;
|
font-size: 1.5em;
|
||||||
font-weight: bold !important;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -68,6 +68,10 @@ class HaPaperSlider extends PaperSliderClass {
|
|||||||
-webkit-transform: scale(1) translate(0, -17px) scaleX(-1) !important;
|
-webkit-transform: scale(1) translate(0, -17px) scaleX(-1) !important;
|
||||||
transform: scale(1) translate(0, -17px) scaleX(-1) !important;
|
transform: scale(1) translate(0, -17px) scaleX(-1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slider-input {
|
||||||
|
width: 54px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
tpl.content.appendChild(styleEl);
|
tpl.content.appendChild(styleEl);
|
||||||
return tpl;
|
return tpl;
|
||||||
|
226
src/components/ha-picture-upload.ts
Normal file
226
src/components/ha-picture-upload.ts
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import "@material/mwc-icon-button/mwc-icon-button";
|
||||||
|
import { mdiClose, mdiImagePlus } from "@mdi/js";
|
||||||
|
import "@polymer/iron-input/iron-input";
|
||||||
|
import "@polymer/paper-input/paper-input-container";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
internalProperty,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
import { classMap } from "lit-html/directives/class-map";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { createImage, generateImageThumbnailUrl } from "../data/image";
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
import "./ha-circular-progress";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
import {
|
||||||
|
showImageCropperDialog,
|
||||||
|
CropOptions,
|
||||||
|
} from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||||
|
|
||||||
|
@customElement("ha-picture-upload")
|
||||||
|
export class HaPictureUpload extends LitElement {
|
||||||
|
public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public value: string | null = null;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public crop = false;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public cropOptions?: CropOptions;
|
||||||
|
|
||||||
|
@property({ type: Number }) public size = 512;
|
||||||
|
|
||||||
|
@internalProperty() private _error = "";
|
||||||
|
|
||||||
|
@internalProperty() private _uploading = false;
|
||||||
|
|
||||||
|
@internalProperty() private _drag = false;
|
||||||
|
|
||||||
|
protected updated(changedProperties: PropertyValues) {
|
||||||
|
if (changedProperties.has("_drag")) {
|
||||||
|
(this.shadowRoot!.querySelector(
|
||||||
|
"paper-input-container"
|
||||||
|
) as any)._setFocused(this._drag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
${this._uploading
|
||||||
|
? html`<ha-circular-progress
|
||||||
|
alt="Uploading"
|
||||||
|
size="large"
|
||||||
|
active
|
||||||
|
></ha-circular-progress>`
|
||||||
|
: html`
|
||||||
|
${this._error ? html`<div class="error">${this._error}</div>` : ""}
|
||||||
|
<label for="input">
|
||||||
|
<paper-input-container
|
||||||
|
.alwaysFloatLabel=${Boolean(this.value)}
|
||||||
|
@drop=${this._handleDrop}
|
||||||
|
@dragenter=${this._handleDragStart}
|
||||||
|
@dragover=${this._handleDragStart}
|
||||||
|
@dragleave=${this._handleDragEnd}
|
||||||
|
@dragend=${this._handleDragEnd}
|
||||||
|
class=${classMap({
|
||||||
|
dragged: this._drag,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<label for="input" slot="label">
|
||||||
|
${this.label ||
|
||||||
|
this.hass.localize("ui.components.picture-upload.label")}
|
||||||
|
</label>
|
||||||
|
<iron-input slot="input">
|
||||||
|
<input
|
||||||
|
id="input"
|
||||||
|
type="file"
|
||||||
|
class="file"
|
||||||
|
accept="image/png, image/jpeg, image/gif"
|
||||||
|
@change=${this._handleFilePicked}
|
||||||
|
/>
|
||||||
|
${this.value ? html`<img .src=${this.value} />` : ""}
|
||||||
|
</iron-input>
|
||||||
|
${this.value
|
||||||
|
? html`<mwc-icon-button
|
||||||
|
slot="suffix"
|
||||||
|
@click=${this._clearPicture}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||||
|
</mwc-icon-button>`
|
||||||
|
: html`<mwc-icon-button slot="suffix">
|
||||||
|
<ha-svg-icon .path=${mdiImagePlus}></ha-svg-icon>
|
||||||
|
</mwc-icon-button>`}
|
||||||
|
</paper-input-container>
|
||||||
|
</label>
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleDrop(ev: DragEvent) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (ev.dataTransfer?.files) {
|
||||||
|
if (this.crop) {
|
||||||
|
this._cropFile(ev.dataTransfer.files[0]);
|
||||||
|
} else {
|
||||||
|
this._uploadFile(ev.dataTransfer.files[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._drag = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleDragStart(ev: DragEvent) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._drag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleDragEnd(ev: DragEvent) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._drag = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleFilePicked(ev) {
|
||||||
|
if (this.crop) {
|
||||||
|
this._cropFile(ev.target.files[0]);
|
||||||
|
} else {
|
||||||
|
this._uploadFile(ev.target.files[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _cropFile(file: File) {
|
||||||
|
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
|
||||||
|
this._error = this.hass.localize(
|
||||||
|
"ui.components.picture-upload.unsupported_format"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showImageCropperDialog(this, {
|
||||||
|
file,
|
||||||
|
options: this.cropOptions || {
|
||||||
|
round: false,
|
||||||
|
aspectRatio: NaN,
|
||||||
|
},
|
||||||
|
croppedCallback: (croppedFile) => {
|
||||||
|
this._uploadFile(croppedFile);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _uploadFile(file: File) {
|
||||||
|
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
|
||||||
|
this._error = this.hass.localize(
|
||||||
|
"ui.components.picture-upload.unsupported_format"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._uploading = true;
|
||||||
|
this._error = "";
|
||||||
|
try {
|
||||||
|
const media = await createImage(this.hass, file);
|
||||||
|
this.value = generateImageThumbnailUrl(media.id, this.size);
|
||||||
|
fireEvent(this, "change");
|
||||||
|
} catch (err) {
|
||||||
|
this._error = err.toString();
|
||||||
|
} finally {
|
||||||
|
this._uploading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearPicture(ev: Event) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.value = null;
|
||||||
|
this._error = "";
|
||||||
|
fireEvent(this, "change");
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
.error {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
paper-input-container {
|
||||||
|
position: relative;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0 -8px;
|
||||||
|
}
|
||||||
|
paper-input-container.dragged:before {
|
||||||
|
position: var(--layout-fit_-_position);
|
||||||
|
top: var(--layout-fit_-_top);
|
||||||
|
right: var(--layout-fit_-_right);
|
||||||
|
bottom: var(--layout-fit_-_bottom);
|
||||||
|
left: var(--layout-fit_-_left);
|
||||||
|
background: currentColor;
|
||||||
|
content: "";
|
||||||
|
opacity: var(--dark-divider-opacity);
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 125px;
|
||||||
|
max-height: 125px;
|
||||||
|
}
|
||||||
|
input.file {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
mwc-icon-button {
|
||||||
|
--mdc-icon-button-size: 24px;
|
||||||
|
--mdc-icon-size: 20px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-picture-upload": HaPictureUpload;
|
||||||
|
}
|
||||||
|
}
|
20
src/components/ha-radio.ts
Normal file
20
src/components/ha-radio.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import "@material/mwc-radio";
|
||||||
|
import type { Radio } from "@material/mwc-radio";
|
||||||
|
import { customElement } from "lit-element";
|
||||||
|
import type { Constructor } from "../types";
|
||||||
|
|
||||||
|
const MwcRadio = customElements.get("mwc-radio") as Constructor<Radio>;
|
||||||
|
|
||||||
|
@customElement("ha-radio")
|
||||||
|
export class HaRadio extends MwcRadio {
|
||||||
|
public firstUpdated() {
|
||||||
|
super.firstUpdated();
|
||||||
|
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-radio": HaRadio;
|
||||||
|
}
|
||||||
|
}
|
@@ -97,6 +97,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
|
|||||||
</h3>
|
</h3>
|
||||||
<a
|
<a
|
||||||
href=${`/config/integrations#config_entry=${relatedConfigEntryId}`}
|
href=${`/config/integrations#config_entry=${relatedConfigEntryId}`}
|
||||||
|
@click=${this._navigateAwayClose}
|
||||||
>
|
>
|
||||||
${this.hass.localize(`component.${entry.domain}.title`)}:
|
${this.hass.localize(`component.${entry.domain}.title`)}:
|
||||||
${entry.title}
|
${entry.title}
|
||||||
@@ -116,7 +117,10 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
|
|||||||
<h3>
|
<h3>
|
||||||
${this.hass.localize("ui.components.related-items.device")}:
|
${this.hass.localize("ui.components.related-items.device")}:
|
||||||
</h3>
|
</h3>
|
||||||
<a href="/config/devices/device/${relatedDeviceId}">
|
<a
|
||||||
|
href="/config/devices/device/${relatedDeviceId}"
|
||||||
|
@click=${this._navigateAwayClose}
|
||||||
|
>
|
||||||
${device.name_by_user || device.name}
|
${device.name_by_user || device.name}
|
||||||
</a>
|
</a>
|
||||||
`;
|
`;
|
||||||
@@ -134,7 +138,10 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
|
|||||||
<h3>
|
<h3>
|
||||||
${this.hass.localize("ui.components.related-items.area")}:
|
${this.hass.localize("ui.components.related-items.area")}:
|
||||||
</h3>
|
</h3>
|
||||||
<a href="/config/areas/area/${relatedAreaId}">
|
<a
|
||||||
|
href="/config/areas/area/${relatedAreaId}"
|
||||||
|
@click=${this._navigateAwayClose}
|
||||||
|
>
|
||||||
${area.name}
|
${area.name}
|
||||||
</a>
|
</a>
|
||||||
`;
|
`;
|
||||||
@@ -278,6 +285,12 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _navigateAwayClose() {
|
||||||
|
// allow new page to open before closing dialog
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
fireEvent(this, "close-dialog");
|
||||||
|
}
|
||||||
|
|
||||||
private async _findRelated() {
|
private async _findRelated() {
|
||||||
this._related = await findRelated(this.hass, this.itemType, this.itemId);
|
this._related = await findRelated(this.hass, this.itemType, this.itemId);
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
|
@@ -16,9 +16,9 @@ class HaServiceDescription extends PolymerElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getDescription(hass, domain, service) {
|
_getDescription(hass, domain, service) {
|
||||||
var domainServices = hass.services[domain];
|
const domainServices = hass.services[domain];
|
||||||
if (!domainServices) return "";
|
if (!domainServices) return "";
|
||||||
var serviceObject = domainServices[service];
|
const serviceObject = domainServices[service];
|
||||||
if (!serviceObject) return "";
|
if (!serviceObject) return "";
|
||||||
return serviceObject.description;
|
return serviceObject.description;
|
||||||
}
|
}
|
||||||
|
62
src/components/ha-settings-row.ts
Normal file
62
src/components/ha-settings-row.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import "@polymer/paper-item/paper-item-body";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
SVGTemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
|
||||||
|
@customElement("ha-settings-row")
|
||||||
|
export class HaSettingsRow extends LitElement {
|
||||||
|
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "three-line" })
|
||||||
|
public threeLine = false;
|
||||||
|
|
||||||
|
protected render(): SVGTemplateResult {
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
paper-item-body {
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<paper-item-body
|
||||||
|
?two-line=${!this.threeLine}
|
||||||
|
?three-line=${this.threeLine}
|
||||||
|
>
|
||||||
|
<slot name="heading"></slot>
|
||||||
|
<div secondary><slot name="description"></slot></div>
|
||||||
|
</paper-item-body>
|
||||||
|
<slot></slot>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
padding: 0 16px;
|
||||||
|
align-content: normal;
|
||||||
|
align-self: auto;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
:host([narrow]) {
|
||||||
|
align-items: normal;
|
||||||
|
flex-direction: column;
|
||||||
|
border-top: 1px solid var(--divider-color);
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
::slotted(ha-switch) {
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-settings-row": HaSettingsRow;
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user