mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-27 13:59:41 +00:00
Compare commits
735 Commits
20171104.0
...
20180710.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c90a3dcdfd | ||
![]() |
5df758c0b2 | ||
![]() |
77c65d43ae | ||
![]() |
dbb6a8e6d4 | ||
![]() |
89ab6a7d5d | ||
![]() |
c378eda17b | ||
![]() |
133d198e7c | ||
![]() |
2e4ddebcda | ||
![]() |
13819937a7 | ||
![]() |
b89ad9b217 | ||
![]() |
e51177b3c2 | ||
![]() |
bb7dc76996 | ||
![]() |
274f8e1b64 | ||
![]() |
d1d3ff9013 | ||
![]() |
c39b6bb665 | ||
![]() |
52f2c29726 | ||
![]() |
b0f29744bf | ||
![]() |
c17f390f58 | ||
![]() |
f58c612018 | ||
![]() |
d96c5f6bde | ||
![]() |
e649d37c05 | ||
![]() |
594c1d6615 | ||
![]() |
10009eea2e | ||
![]() |
cb60904912 | ||
![]() |
ddd37f63a0 | ||
![]() |
d71a80c4f8 | ||
![]() |
6b10eeb1e9 | ||
![]() |
e3d82f9e37 | ||
![]() |
6739c88428 | ||
![]() |
5b0ca026d5 | ||
![]() |
42525ae1df | ||
![]() |
c201a9073f | ||
![]() |
ad4814dfad | ||
![]() |
e5c900aec1 | ||
![]() |
6f8385afa7 | ||
![]() |
5cccf4836f | ||
![]() |
d299166f2a | ||
![]() |
c0ae7d50ad | ||
![]() |
dc034038c0 | ||
![]() |
536b1e7b73 | ||
![]() |
20d6f6d530 | ||
![]() |
173cd8126c | ||
![]() |
e452384ecd | ||
![]() |
918c2ce29e | ||
![]() |
66803cd4eb | ||
![]() |
66ae61374d | ||
![]() |
cf5460bf58 | ||
![]() |
c868df2718 | ||
![]() |
25fddad446 | ||
![]() |
890dc0512b | ||
![]() |
6b8fbd91c6 | ||
![]() |
06cf03bff0 | ||
![]() |
22ee205807 | ||
![]() |
7bbe8dc2f3 | ||
![]() |
e410274684 | ||
![]() |
76256699e0 | ||
![]() |
dc5213a79a | ||
![]() |
54fcb21917 | ||
![]() |
1a9af5595f | ||
![]() |
5f44009177 | ||
![]() |
42026f096f | ||
![]() |
122414e7bd | ||
![]() |
c781163f6b | ||
![]() |
cdb7a6261e | ||
![]() |
a8063f3359 | ||
![]() |
bb670b76a9 | ||
![]() |
05816374a8 | ||
![]() |
5b67a3691a | ||
![]() |
fb2fc9fbbb | ||
![]() |
ee0308849e | ||
![]() |
51f169aa13 | ||
![]() |
038f5b644b | ||
![]() |
3442700e1f | ||
![]() |
07d76739b4 | ||
![]() |
d40dea6d3b | ||
![]() |
8bbc8e0bb8 | ||
![]() |
9e2d311ce6 | ||
![]() |
de91aea3b9 | ||
![]() |
e0ccc1999a | ||
![]() |
b55004d73b | ||
![]() |
3d18462ec0 | ||
![]() |
654e74294d | ||
![]() |
20e92893e0 | ||
![]() |
f2d30ad850 | ||
![]() |
88453733eb | ||
![]() |
584e959f1a | ||
![]() |
479de6c4c7 | ||
![]() |
be7900cd87 | ||
![]() |
7bfc01b02c | ||
![]() |
590bba0aca | ||
![]() |
aa27ee609d | ||
![]() |
e1e88aa8b2 | ||
![]() |
e3d7220c0b | ||
![]() |
9fd1db0493 | ||
![]() |
748b5a8e41 | ||
![]() |
8991c966d2 | ||
![]() |
2cfff991ac | ||
![]() |
376228e0fe | ||
![]() |
4c1feb313b | ||
![]() |
b0561d7766 | ||
![]() |
bda2d42994 | ||
![]() |
d225105e58 | ||
![]() |
313a3dd2c9 | ||
![]() |
c579d7fed3 | ||
![]() |
c83184654e | ||
![]() |
7e0be4a2f6 | ||
![]() |
53e698c757 | ||
![]() |
8d5c862908 | ||
![]() |
d5266c1c56 | ||
![]() |
44d64bc7ce | ||
![]() |
545af4c6d2 | ||
![]() |
501033df15 | ||
![]() |
1f82934c93 | ||
![]() |
9d22645e87 | ||
![]() |
b312533948 | ||
![]() |
77f623d519 | ||
![]() |
8cebddcccc | ||
![]() |
75502bac6e | ||
![]() |
18b52b53cb | ||
![]() |
b6ee5442f0 | ||
![]() |
691b80c08a | ||
![]() |
f8b38ced26 | ||
![]() |
63b123fc8f | ||
![]() |
a92c187627 | ||
![]() |
92111cd5be | ||
![]() |
1f8f6f52bc | ||
![]() |
626b054540 | ||
![]() |
4acfa2ba88 | ||
![]() |
87bd9ed48a | ||
![]() |
ce4280e816 | ||
![]() |
3382430c8f | ||
![]() |
c186a5aab2 | ||
![]() |
21dcbb3b9d | ||
![]() |
cd33e2568a | ||
![]() |
4cf7959b12 | ||
![]() |
dc9a227aa3 | ||
![]() |
2a8462951b | ||
![]() |
449751c59f | ||
![]() |
d2eb0ef23f | ||
![]() |
d752060f7a | ||
![]() |
cc74374390 | ||
![]() |
dd87502688 | ||
![]() |
4e608e6a2c | ||
![]() |
ba2c3edc87 | ||
![]() |
13c8a00e97 | ||
![]() |
1076fd8fc4 | ||
![]() |
5f226d1809 | ||
![]() |
75e3f1f37b | ||
![]() |
b43b34263e | ||
![]() |
f79feae120 | ||
![]() |
052e659782 | ||
![]() |
890c31fb96 | ||
![]() |
58a0f6aab9 | ||
![]() |
2905cefd39 | ||
![]() |
67cef55f34 | ||
![]() |
710d98feda | ||
![]() |
b085121ab4 | ||
![]() |
274e1a671f | ||
![]() |
98df34c0a8 | ||
![]() |
e28c651930 | ||
![]() |
8e8d907090 | ||
![]() |
cf3d864378 | ||
![]() |
7f133d0316 | ||
![]() |
9717166fee | ||
![]() |
0b6214be2b | ||
![]() |
7f93317314 | ||
![]() |
b89de4b494 | ||
![]() |
d243f2ead6 | ||
![]() |
92930a2b94 | ||
![]() |
7a2aff712b | ||
![]() |
0ae676d9ba | ||
![]() |
673f769237 | ||
![]() |
bb442c824b | ||
![]() |
ab16a3712e | ||
![]() |
b71a4be67e | ||
![]() |
eb91cedf68 | ||
![]() |
9ac1384e1f | ||
![]() |
cab53b3324 | ||
![]() |
bf4d0e6bc9 | ||
![]() |
10c997b7b2 | ||
![]() |
4d48a63141 | ||
![]() |
af14fc6548 | ||
![]() |
4bd14a5280 | ||
![]() |
cf57cf853b | ||
![]() |
8133102bcb | ||
![]() |
dbcae9cb77 | ||
![]() |
cb284d9718 | ||
![]() |
d16f4c846a | ||
![]() |
ce3564625e | ||
![]() |
e928be353a | ||
![]() |
addb8e3111 | ||
![]() |
fa11fbc85d | ||
![]() |
c3d67133c2 | ||
![]() |
1a3966e55f | ||
![]() |
e11cca28fd | ||
![]() |
1f14373117 | ||
![]() |
490c37e899 | ||
![]() |
3c48973584 | ||
![]() |
b378b92aa8 | ||
![]() |
c0919cfe11 | ||
![]() |
f96db5003a | ||
![]() |
a8fa8d46e5 | ||
![]() |
673c7c5184 | ||
![]() |
ff50414cbb | ||
![]() |
b8ac150ee4 | ||
![]() |
2f6bb9af0c | ||
![]() |
348943f221 | ||
![]() |
74e0779d38 | ||
![]() |
21e4bc4ee4 | ||
![]() |
af6167b9c8 | ||
![]() |
eb29b73390 | ||
![]() |
e158709b1e | ||
![]() |
045b1d02be | ||
![]() |
66012da4de | ||
![]() |
294f0febbf | ||
![]() |
a79f98efbf | ||
![]() |
dd57ddbef6 | ||
![]() |
d3b6740488 | ||
![]() |
f51503af4f | ||
![]() |
0c92b356a1 | ||
![]() |
9393bb2fba | ||
![]() |
61bc2d04f3 | ||
![]() |
71196b9704 | ||
![]() |
29912d1b63 | ||
![]() |
2d67a01ec2 | ||
![]() |
9d8f055f7b | ||
![]() |
c6f2f43767 | ||
![]() |
f4c36e37bb | ||
![]() |
d3c10d9b49 | ||
![]() |
4e9a7a4a23 | ||
![]() |
f24a26c686 | ||
![]() |
df384dc23e | ||
![]() |
008fcbe1dc | ||
![]() |
86a522ce65 | ||
![]() |
6bf34afc31 | ||
![]() |
081e8d9824 | ||
![]() |
8e6929659d | ||
![]() |
4d50ab937a | ||
![]() |
0edf06bfb9 | ||
![]() |
d1fcdfd5a3 | ||
![]() |
d7b2a03880 | ||
![]() |
4a734fbffc | ||
![]() |
41990767e2 | ||
![]() |
0789c0884c | ||
![]() |
4de7cbec30 | ||
![]() |
1d144a101c | ||
![]() |
1eda9155fd | ||
![]() |
43b0be9581 | ||
![]() |
52b6fe006f | ||
![]() |
365d660e79 | ||
![]() |
34ec3e0ae5 | ||
![]() |
059eda861e | ||
![]() |
5ede26f162 | ||
![]() |
960bdc0c9b | ||
![]() |
81fbda49bd | ||
![]() |
e57d9f7751 | ||
![]() |
964ada87b7 | ||
![]() |
ba3670c5db | ||
![]() |
20ea9e5df7 | ||
![]() |
e1c9f3deea | ||
![]() |
dba388f723 | ||
![]() |
3d1164f09c | ||
![]() |
bc27f854f1 | ||
![]() |
cb0db95abe | ||
![]() |
8ac08bc802 | ||
![]() |
f70c0aea6c | ||
![]() |
d1adc2fed0 | ||
![]() |
1971223ad3 | ||
![]() |
3fa9896543 | ||
![]() |
7b3b717f43 | ||
![]() |
405cb36904 | ||
![]() |
0c6f8c34fb | ||
![]() |
23a2a479a5 | ||
![]() |
5a16095270 | ||
![]() |
89cb8c87ae | ||
![]() |
96d7ec7cda | ||
![]() |
47642957c8 | ||
![]() |
65780de61e | ||
![]() |
646f0bb718 | ||
![]() |
68fb35a401 | ||
![]() |
658b349755 | ||
![]() |
a4afc2e37a | ||
![]() |
205d6a8347 | ||
![]() |
89333aa55e | ||
![]() |
2a3325efd7 | ||
![]() |
3b7a206cec | ||
![]() |
912969111f | ||
![]() |
9116f5733d | ||
![]() |
0fdf980fee | ||
![]() |
8ecc41388a | ||
![]() |
356e104096 | ||
![]() |
eeba117e4b | ||
![]() |
3da55c6ab5 | ||
![]() |
faed5fbfe4 | ||
![]() |
6f738510fa | ||
![]() |
de55c13355 | ||
![]() |
38e1b16031 | ||
![]() |
06341edc49 | ||
![]() |
9fc3d9d019 | ||
![]() |
3e90db5fa3 | ||
![]() |
30555eda88 | ||
![]() |
ac38fdb9df | ||
![]() |
4c6d9602ae | ||
![]() |
811d99b68c | ||
![]() |
a983a5dbc5 | ||
![]() |
f21db486eb | ||
![]() |
e1b924a154 | ||
![]() |
421b9ec800 | ||
![]() |
e8b84c6d52 | ||
![]() |
86db23a957 | ||
![]() |
a22b62cf2a | ||
![]() |
db2f588e86 | ||
![]() |
02cf337f1a | ||
![]() |
eafd7fb296 | ||
![]() |
c0e3f8ec6b | ||
![]() |
510cc6448e | ||
![]() |
a832566715 | ||
![]() |
75ae672ef9 | ||
![]() |
570cb5d52b | ||
![]() |
afc4663870 | ||
![]() |
a5aaaf6a0a | ||
![]() |
288478978f | ||
![]() |
e719f113d9 | ||
![]() |
2aa02ff561 | ||
![]() |
0a60ec2298 | ||
![]() |
cd3136908c | ||
![]() |
6d547aaf21 | ||
![]() |
85c4c51228 | ||
![]() |
5667f06a12 | ||
![]() |
5dc9efd995 | ||
![]() |
a2ec19e10f | ||
![]() |
8047313165 | ||
![]() |
5068178aac | ||
![]() |
a597749020 | ||
![]() |
1335a74605 | ||
![]() |
027165c8ac | ||
![]() |
a512d9e910 | ||
![]() |
f385b6c80d | ||
![]() |
fdfa09ed2e | ||
![]() |
dc6ca7c774 | ||
![]() |
f40d64d68c | ||
![]() |
961f43e4a5 | ||
![]() |
2cf508e0b0 | ||
![]() |
9d3b5c9d9b | ||
![]() |
57e500b109 | ||
![]() |
d5776f750f | ||
![]() |
bfaebfe418 | ||
![]() |
9cff9cac10 | ||
![]() |
7967ab307c | ||
![]() |
b57116f39c | ||
![]() |
51f4028343 | ||
![]() |
c83a05c1ea | ||
![]() |
b102925323 | ||
![]() |
1cda7791c1 | ||
![]() |
efa956a0b5 | ||
![]() |
0a5767913a | ||
![]() |
859ffdb66d | ||
![]() |
4c65767e5a | ||
![]() |
d7d167fbf9 | ||
![]() |
6447e98caa | ||
![]() |
d8fb01ab44 | ||
![]() |
77330d03b3 | ||
![]() |
88ad376045 | ||
![]() |
ed0696ea9c | ||
![]() |
5508805f0b | ||
![]() |
d2688cab75 | ||
![]() |
06502cb93a | ||
![]() |
0e227708b9 | ||
![]() |
e46d2e2934 | ||
![]() |
caf1c2fdd1 | ||
![]() |
209a118833 | ||
![]() |
0790cd1ac9 | ||
![]() |
fb8fb09c73 | ||
![]() |
7081fb5123 | ||
![]() |
6c2126fb5f | ||
![]() |
af1581b4c5 | ||
![]() |
38542e1f0c | ||
![]() |
d87d845dbc | ||
![]() |
7be6d17b37 | ||
![]() |
981e94e84d | ||
![]() |
c04c30337f | ||
![]() |
8451f9487e | ||
![]() |
41e7235ccf | ||
![]() |
3b76238241 | ||
![]() |
796ec4a4b0 | ||
![]() |
6ee9808d42 | ||
![]() |
399d14a5e0 | ||
![]() |
42f3294523 | ||
![]() |
d609ce241e | ||
![]() |
e5a5c353c4 | ||
![]() |
0df4aa6117 | ||
![]() |
6bdf1c8b80 | ||
![]() |
421b9abc7d | ||
![]() |
b107e0e15c | ||
![]() |
fe6c8faad7 | ||
![]() |
46a2f4fd72 | ||
![]() |
3324c7e01c | ||
![]() |
8146794857 | ||
![]() |
b03c361198 | ||
![]() |
860860099e | ||
![]() |
b80c52dab2 | ||
![]() |
e0ca88b3ad | ||
![]() |
196ea97917 | ||
![]() |
4f55103a12 | ||
![]() |
9082b47687 | ||
![]() |
05da295f27 | ||
![]() |
9dc809cdb9 | ||
![]() |
09fe2172b2 | ||
![]() |
3d101116a2 | ||
![]() |
2ed5b7e7c1 | ||
![]() |
7066b40e4c | ||
![]() |
2c79094fb4 | ||
![]() |
198e2dd11f | ||
![]() |
3234777154 | ||
![]() |
0ec4cd668f | ||
![]() |
f1736024cc | ||
![]() |
29f9b2d201 | ||
![]() |
97eec435ab | ||
![]() |
b52d2967df | ||
![]() |
964b048a93 | ||
![]() |
a3ac015f84 | ||
![]() |
85ab32d752 | ||
![]() |
a1dbd18a9a | ||
![]() |
08d4e0af18 | ||
![]() |
1fbad393bf | ||
![]() |
675a3adfbe | ||
![]() |
d0fc088c06 | ||
![]() |
ca6ec8bb0f | ||
![]() |
f9b7268b9a | ||
![]() |
bec9162198 | ||
![]() |
710139bf4b | ||
![]() |
e41b5238c2 | ||
![]() |
d188821765 | ||
![]() |
19705a9c2b | ||
![]() |
addad74019 | ||
![]() |
9dc33de49f | ||
![]() |
39172f8c49 | ||
![]() |
101794c88e | ||
![]() |
bae1ab822c | ||
![]() |
f87dbc5735 | ||
![]() |
7377db312b | ||
![]() |
bb6c46180d | ||
![]() |
3ddb456970 | ||
![]() |
3fbd5e351e | ||
![]() |
9c6d28fbc4 | ||
![]() |
5966e872cf | ||
![]() |
9ca00d1c85 | ||
![]() |
16734c7423 | ||
![]() |
26b9ddf3ed | ||
![]() |
73608a73e7 | ||
![]() |
c2b090b912 | ||
![]() |
f75147c799 | ||
![]() |
5296dcfe85 | ||
![]() |
24bafceb71 | ||
![]() |
579adb0455 | ||
![]() |
3430996700 | ||
![]() |
ebc21aaa40 | ||
![]() |
a88c6f49a2 | ||
![]() |
d27d9dd5da | ||
![]() |
807837a0dc | ||
![]() |
bb6b0ff714 | ||
![]() |
1ba34b1f78 | ||
![]() |
44c325a929 | ||
![]() |
294dec59a7 | ||
![]() |
d4b257854d | ||
![]() |
9f3458fcd1 | ||
![]() |
ff658c86ff | ||
![]() |
0ab240c678 | ||
![]() |
672bfb375f | ||
![]() |
cdb9936795 | ||
![]() |
8b719778d0 | ||
![]() |
1a18ee2755 | ||
![]() |
5f5ac3834d | ||
![]() |
cdd4cabb4b | ||
![]() |
b47c5beacf | ||
![]() |
0ffb31999e | ||
![]() |
c149ac735a | ||
![]() |
bb946d9eec | ||
![]() |
ea57e71c8b | ||
![]() |
346022c48e | ||
![]() |
0a070a3fda | ||
![]() |
24c9dd0472 | ||
![]() |
9f27f75397 | ||
![]() |
35c8c70783 | ||
![]() |
6a5de599df | ||
![]() |
72a0b3520d | ||
![]() |
7acab579b4 | ||
![]() |
9de71db3cb | ||
![]() |
f3c3bb8c43 | ||
![]() |
012e0981f2 | ||
![]() |
83e34f3f95 | ||
![]() |
f83a9d7339 | ||
![]() |
5731c1fa28 | ||
![]() |
7860133709 | ||
![]() |
1783696ecb | ||
![]() |
10c07673c1 | ||
![]() |
e2ff04e40b | ||
![]() |
932d8afbed | ||
![]() |
1cf18a34b8 | ||
![]() |
7f461defc1 | ||
![]() |
4c5d85746c | ||
![]() |
b6ad4edd32 | ||
![]() |
c6030e6edc | ||
![]() |
500edbad0d | ||
![]() |
3701e022bc | ||
![]() |
be3f35c8cd | ||
![]() |
81c49628e4 | ||
![]() |
ac628787ac | ||
![]() |
6ce72444ae | ||
![]() |
710c2f1094 | ||
![]() |
1f703fbdda | ||
![]() |
76153d1e17 | ||
![]() |
ec930d2c56 | ||
![]() |
9e09d5b095 | ||
![]() |
6ad0c254b5 | ||
![]() |
a6340fb856 | ||
![]() |
21ee9b297d | ||
![]() |
da807dc508 | ||
![]() |
384d5fc8a9 | ||
![]() |
fcea1fa57d | ||
![]() |
4f99bd6811 | ||
![]() |
0256f73404 | ||
![]() |
a1b578f81e | ||
![]() |
13f8fa7e11 | ||
![]() |
c11a525a2d | ||
![]() |
d21c1bc615 | ||
![]() |
42d11c5a3f | ||
![]() |
41fe6e8021 | ||
![]() |
c3e35a27ba | ||
![]() |
89464c16ff | ||
![]() |
9c2f6e591d | ||
![]() |
3c95559f33 | ||
![]() |
50ed7678a1 | ||
![]() |
8649c5352b | ||
![]() |
31bc099cef | ||
![]() |
447dd6640f | ||
![]() |
31d2b6ffe1 | ||
![]() |
aced689207 | ||
![]() |
3736d45318 | ||
![]() |
5749e2f07c | ||
![]() |
783f356679 | ||
![]() |
85d58ba134 | ||
![]() |
8b8ba5875f | ||
![]() |
9131a7c7e3 | ||
![]() |
3e0193c704 | ||
![]() |
40731152e9 | ||
![]() |
811e9e2a0e | ||
![]() |
4029508b3f | ||
![]() |
6fd8ad52b0 | ||
![]() |
fbe44598ac | ||
![]() |
58b2a28fe5 | ||
![]() |
0b47d1f6a5 | ||
![]() |
d6fd21521c | ||
![]() |
e9dfa79f36 | ||
![]() |
728d781843 | ||
![]() |
f2358acf2d | ||
![]() |
c8e4ac422b | ||
![]() |
48b0857edb | ||
![]() |
c06be58a33 | ||
![]() |
7231976af6 | ||
![]() |
ea16ebd4f0 | ||
![]() |
0b9bd62251 | ||
![]() |
1d13126bb5 | ||
![]() |
27d343b488 | ||
![]() |
0a091a272c | ||
![]() |
5728d8ad1b | ||
![]() |
bdea42f0b7 | ||
![]() |
fdcc73c6cc | ||
![]() |
9e2396375e | ||
![]() |
5085c78f7e | ||
![]() |
eeb949a081 | ||
![]() |
0fd84a2f8d | ||
![]() |
4379df0d5c | ||
![]() |
cf4d867fa1 | ||
![]() |
b3ded276b5 | ||
![]() |
38088acf14 | ||
![]() |
1b60a93fcc | ||
![]() |
e7df8cb195 | ||
![]() |
440145ab1b | ||
![]() |
0c840e1751 | ||
![]() |
36c658096a | ||
![]() |
34fd3e4899 | ||
![]() |
b16bc88eb5 | ||
![]() |
60ac82edc5 | ||
![]() |
f0f1a56537 | ||
![]() |
097a8cfdc6 | ||
![]() |
1b66492db9 | ||
![]() |
e202f08193 | ||
![]() |
6b180988fd | ||
![]() |
c8c21e6fac | ||
![]() |
af8f77779b | ||
![]() |
1aa1ac709d | ||
![]() |
91fadccf33 | ||
![]() |
0904af2ad2 | ||
![]() |
aa5ff72710 | ||
![]() |
f8261d93d3 | ||
![]() |
f385c7e7d5 | ||
![]() |
788650f8e5 | ||
![]() |
0018a9a9c8 | ||
![]() |
bf126b6c5e | ||
![]() |
c18247cd6b | ||
![]() |
fa2cc68139 | ||
![]() |
8dde92d572 | ||
![]() |
baccd6fb88 | ||
![]() |
b99d9923ea | ||
![]() |
9d0e89e792 | ||
![]() |
13f5e33087 | ||
![]() |
a061a58494 | ||
![]() |
9b3448d44c | ||
![]() |
9ae2325834 | ||
![]() |
688de2ff2d | ||
![]() |
e0a63a2ee3 | ||
![]() |
2765c88d3f | ||
![]() |
b092cdd04d | ||
![]() |
a723c62f4f | ||
![]() |
c1e7f4cc77 | ||
![]() |
b0837059d4 | ||
![]() |
8dc81b1daa | ||
![]() |
77cc77396b | ||
![]() |
7303e55f63 | ||
![]() |
b73c3ed233 | ||
![]() |
f9cd2d9612 | ||
![]() |
90e6f59a74 | ||
![]() |
27046b00c6 | ||
![]() |
7cfa694980 | ||
![]() |
640e6eb1ef | ||
![]() |
7d20d8fe71 | ||
![]() |
2680a3f7e3 | ||
![]() |
326fa00365 | ||
![]() |
8054aa744e | ||
![]() |
10ddb7faac | ||
![]() |
288ffad23a | ||
![]() |
9aaf50b089 | ||
![]() |
de5a33d1c7 | ||
![]() |
edf8cbb95b | ||
![]() |
70d09641f8 | ||
![]() |
bc94dce8f7 | ||
![]() |
48ecfe07a2 | ||
![]() |
958c5bf935 | ||
![]() |
db0438dd4d | ||
![]() |
3ba15cb7b5 | ||
![]() |
69eb10c6dd | ||
![]() |
508e1fd737 | ||
![]() |
0707528bd7 | ||
![]() |
28457747e7 | ||
![]() |
5f5a62d094 | ||
![]() |
80a11206af | ||
![]() |
c330b87506 | ||
![]() |
a960559639 | ||
![]() |
6c2cd420f5 | ||
![]() |
95288f8c3d | ||
![]() |
2cfda880ac | ||
![]() |
713117d4d9 | ||
![]() |
db630677a4 | ||
![]() |
3a100bff23 | ||
![]() |
ea4fd25330 | ||
![]() |
0dd1d4f478 | ||
![]() |
57ecbf27ca | ||
![]() |
3d90d1d016 | ||
![]() |
3412edb843 | ||
![]() |
7e77a7c32c | ||
![]() |
cb5c9b3f3f | ||
![]() |
8d790e9601 | ||
![]() |
2ff0be8529 | ||
![]() |
0b9e7d5fa2 | ||
![]() |
e5974ab71b | ||
![]() |
5a65fd7526 | ||
![]() |
4f18bdf0ea | ||
![]() |
fbc9755796 | ||
![]() |
d658beacea | ||
![]() |
01fab1075e | ||
![]() |
415b0b127f | ||
![]() |
1a71ee5af3 | ||
![]() |
1c2d713846 | ||
![]() |
b15edbd4fd | ||
![]() |
d79ae551b2 | ||
![]() |
6074de356c | ||
![]() |
72f22f6214 | ||
![]() |
c0df1e2a89 | ||
![]() |
1af77e682d | ||
![]() |
7db89d5bc2 | ||
![]() |
ef5155984f | ||
![]() |
8f0ebcb69d | ||
![]() |
70c082716f | ||
![]() |
3ff9fe1041 | ||
![]() |
41e97a6f83 | ||
![]() |
8078158a56 | ||
![]() |
57997be342 | ||
![]() |
6fac4e9027 | ||
![]() |
de87c5b19b | ||
![]() |
8cf0c0e94d | ||
![]() |
675a7a3b86 | ||
![]() |
a4bcf062d5 | ||
![]() |
056e9e0d74 | ||
![]() |
926c46b701 | ||
![]() |
1314caba97 | ||
![]() |
1ab551116e | ||
![]() |
1f9fc46576 | ||
![]() |
3701683d4b | ||
![]() |
f106767eae | ||
![]() |
b7d2b2fcfd | ||
![]() |
508b5d6d77 | ||
![]() |
adac8e55d7 | ||
![]() |
a2612af6a9 | ||
![]() |
67e040ad8e | ||
![]() |
152df2297a | ||
![]() |
6de540a0b1 | ||
![]() |
583abedd34 | ||
![]() |
86128d54b4 | ||
![]() |
f81429702c | ||
![]() |
5722b6bbdb | ||
![]() |
bac3d8c17e | ||
![]() |
9f6edeec6e | ||
![]() |
11f4f3b3c9 | ||
![]() |
4f4224953f | ||
![]() |
f600a8e7f4 | ||
![]() |
aa389bf206 | ||
![]() |
a8b5d07d66 | ||
![]() |
f16886f63d | ||
![]() |
d1325da6e6 | ||
![]() |
95031fdd79 | ||
![]() |
fe439723ee | ||
![]() |
798a2bbd34 | ||
![]() |
0dc3bc7926 | ||
![]() |
3025b6049f | ||
![]() |
ba5f401890 | ||
![]() |
db9dc653e0 | ||
![]() |
9b0f4fa234 | ||
![]() |
2596d2ba52 | ||
![]() |
904c3db3ea | ||
![]() |
90b80880ed | ||
![]() |
d2faeaffe7 | ||
![]() |
6959b1849f | ||
![]() |
c3a6495eb1 | ||
![]() |
fdf2fa3d3f |
@@ -1,6 +1,4 @@
|
||||
node_modules
|
||||
bower_components
|
||||
hass_frontend
|
||||
build
|
||||
build-temp
|
||||
hass_frontend_es5
|
||||
.git
|
||||
|
@@ -9,16 +9,26 @@
|
||||
"settings": {
|
||||
"react": {
|
||||
"pragma": "h"
|
||||
},
|
||||
"import/resolver": {
|
||||
"webpack": {
|
||||
"config": "webpack.config.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"__DEV__": false,
|
||||
"__DEMO__": false,
|
||||
"__BUILD__": false,
|
||||
"__VERSION__": false,
|
||||
"__PUBLIC_PATH__": false,
|
||||
"Polymer": true,
|
||||
"webkitSpeechRecognition": false,
|
||||
"ResizeObserver": false
|
||||
},
|
||||
"env": {
|
||||
"browser": true
|
||||
"browser": true,
|
||||
"mocha": true
|
||||
},
|
||||
"rules": {
|
||||
"class-methods-use-this": 0,
|
||||
@@ -40,10 +50,15 @@
|
||||
"no-multi-assign": 0,
|
||||
"radix": 0,
|
||||
"no-alert": 0,
|
||||
"no-return-await": 0,
|
||||
"prefer-destructuring": 0,
|
||||
"no-restricted-globals": 0,
|
||||
"no-restricted-globals": [2, "event"],
|
||||
"prefer-promise-reject-errors": 0,
|
||||
"import/prefer-default-export": 0,
|
||||
"import/no-unresolved": 0,
|
||||
"import/extensions": [2, "ignorePackages"],
|
||||
"object-curly-newline": 0,
|
||||
"default-case": 0,
|
||||
"react/jsx-no-bind": [2, { "ignoreRefs": true }],
|
||||
"react/jsx-no-duplicate-props": 2,
|
||||
"react/self-closing-comp": 2,
|
||||
@@ -56,10 +71,10 @@
|
||||
"react/jsx-curly-spacing": 2,
|
||||
"react/jsx-no-undef": 2,
|
||||
"react/jsx-uses-react": 2,
|
||||
"react/jsx-uses-vars": 2
|
||||
"react/jsx-uses-vars": 2,
|
||||
"no-restricted-syntax": [0, "ForOfStatement"]
|
||||
},
|
||||
"plugins": [
|
||||
"html",
|
||||
"react"
|
||||
]
|
||||
}
|
14
.eslintrc.json
Normal file
14
.eslintrc.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "./.eslintrc-hound.json",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"rules": {
|
||||
"import/no-unresolved": 2,
|
||||
"linebreak-style": 0
|
||||
}
|
||||
}
|
33
.github/ISSUE_TEMPLATE.md
vendored
Normal file
33
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
<!-- READ THIS FIRST:
|
||||
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
||||
- Provide as many details as possible. Do not delete any text from this template!
|
||||
-->
|
||||
|
||||
**Home Assistant release with the issue:**
|
||||
<!--
|
||||
- Frontend -> Developer tools -> Info
|
||||
- Or use this command: hass --version
|
||||
-->
|
||||
|
||||
**Last working Home Assistant release (if known):**
|
||||
|
||||
|
||||
**Browser and Operating System:**
|
||||
<!--
|
||||
Provide details about what browser (and version) you are seeing the issue in. And also which operating system this is on. If possible try to replicate the issue in other browsers and include your findings here.
|
||||
-->
|
||||
|
||||
**Description of problem:**
|
||||
<!--
|
||||
Explain what the issue is, and how things should look/behave. If possible provide a screenshot with a description.
|
||||
-->
|
||||
|
||||
|
||||
**Javascript errors shown in the web inspector (if applicable):**
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
**Additional information:**
|
13
.github/move.yml
vendored
Normal file
13
.github/move.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Configuration for move-issues - https://github.com/dessant/move-issues
|
||||
|
||||
# Delete the command comment. Ignored when the comment also contains other content
|
||||
deleteCommand: true
|
||||
# Close the source issue after moving
|
||||
closeSourceIssue: true
|
||||
# Lock the source issue after moving
|
||||
lockSourceIssue: false
|
||||
# Set custom aliases for targets
|
||||
# aliases:
|
||||
# r: repo
|
||||
# or: owner/repo
|
||||
|
4
.github/release-drafter.yml
vendored
Normal file
4
.github/release-drafter.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
template: |
|
||||
## What's Changed
|
||||
|
||||
$CHANGES
|
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,10 +1,11 @@
|
||||
build/*
|
||||
build-temp/*
|
||||
build
|
||||
build-translations/*
|
||||
node_modules/*
|
||||
bower_components/*
|
||||
npm-debug.log
|
||||
.DS_Store
|
||||
hass_frontend/*
|
||||
hass_frontend_es5/*
|
||||
.reify-cache
|
||||
|
||||
# Python stuff
|
||||
*.py[cod]
|
||||
@@ -19,3 +20,7 @@ venv
|
||||
lib
|
||||
bin
|
||||
dist
|
||||
|
||||
# Secrets
|
||||
.lokalise_token
|
||||
yarn-error.log
|
||||
|
@@ -3,4 +3,4 @@ jshint:
|
||||
|
||||
eslint:
|
||||
enabled: true
|
||||
config_file: .eslintrc
|
||||
config_file: .eslintrc-hound.json
|
||||
|
28
.travis.yml
28
.travis.yml
@@ -3,22 +3,24 @@ language: node_js
|
||||
cache:
|
||||
yarn: true
|
||||
directories:
|
||||
- bower_components
|
||||
install:
|
||||
- yarn install
|
||||
- ./node_modules/.bin/bower install
|
||||
addons:
|
||||
firefox: latest
|
||||
apt:
|
||||
sources:
|
||||
- google-chrome
|
||||
packages:
|
||||
- google-chrome-stable
|
||||
- bower_components
|
||||
install: yarn install
|
||||
script:
|
||||
- npm run build
|
||||
- hassio/script/build_hassio
|
||||
- npm run test
|
||||
- xvfb-run wct
|
||||
- if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --plugin sauce; fi
|
||||
# - xvfb-run wct --module-resolution=node --npm
|
||||
# - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --module-resolution=node --npm --plugin sauce; fi'
|
||||
services:
|
||||
- docker
|
||||
before_deploy:
|
||||
- 'docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21'
|
||||
deploy:
|
||||
provider: script
|
||||
script: script/travis_deploy
|
||||
'on':
|
||||
branch: master
|
||||
dist: trusty
|
||||
addons:
|
||||
sauce_connect: true
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
include README.md
|
||||
include LICENSE.md
|
||||
graft hass_frontend
|
||||
graft hass_frontend_es5
|
||||
recursive-exclude * *.py[co]
|
||||
|
13
README.md
13
README.md
@@ -1,17 +1,12 @@
|
||||
# Home Assistant Polymer [](https://travis-ci.org/home-assistant/home-assistant-polymer)
|
||||
|
||||
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend. The frontend is built on top of the following technologies:
|
||||
|
||||
* [Websockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
|
||||
* [Polymer](https://www.polymer-project.org/)
|
||||
* [Rollup](http://rollupjs.org/) to package Home Assistant JS
|
||||
* [Bower](https://bower.io) for Polymer package management
|
||||
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
|
||||
|
||||
[](https://home-assistant.io/demo/)
|
||||
|
||||
[View demo of the Polymer frontend](https://home-assistant.io/demo/)
|
||||
[More information about Home Assistant](https://home-assistant.io)
|
||||
[Frontend development instructions](https://home-assistant.io/developers/frontend/)
|
||||
- [View demo of the Polymer frontend](https://home-assistant.io/demo/)
|
||||
- [More information about Home Assistant](https://home-assistant.io)
|
||||
- [Frontend development instructions](https://developers.home-assistant.io/docs/en/frontend_index.html)
|
||||
|
||||
## License
|
||||
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
|
||||
|
11
bower.json
11
bower.json
@@ -12,16 +12,15 @@
|
||||
"app-localize-behavior": "PolymerElements/app-localize-behavior#~2.0.0",
|
||||
"app-route": "PolymerElements/app-route#^2.0.0",
|
||||
"app-storage": "^2.0.2",
|
||||
"fecha": "~2.3.0",
|
||||
"font-roboto-local": "~1.0.1",
|
||||
"font-roboto": "PolymerElements/font-roboto-local#~1.0.1",
|
||||
"google-apis": "GoogleWebComponents/google-apis#~2.0.0",
|
||||
"iron-autogrow-textarea": "PolymerElements/iron-autogrow-textarea#^2.0.0",
|
||||
"iron-flex-layout": "PolymerElements/iron-flex-layout#^2.0.0",
|
||||
"iron-icon": "PolymerElements/iron-icon#^2.0.0",
|
||||
"iron-image": "PolymerElements/iron-image#^2.1.1",
|
||||
"iron-input": "PolymerElements/iron-input#^2.0.0",
|
||||
"iron-media-query": "PolymerElements/iron-media-query#^2.0.0",
|
||||
"iron-label": "PolymerElements/iron-label#^2.0.0",
|
||||
"iron-pages": "PolymerElements/iron-pages#^2.0.0",
|
||||
"leaflet": "^1.0.2",
|
||||
"neon-animation": "PolymerElements/neon-animation#^2.0.1",
|
||||
@@ -51,10 +50,14 @@
|
||||
"paper-toast": "PolymerElements/paper-toast#^2.0.0",
|
||||
"paper-toggle-button": "PolymerElements/paper-toggle-button#^2.0.0",
|
||||
"polymer": "^2.1.1",
|
||||
"vaadin-combo-box": "vaadin/vaadin-combo-box#^2.0.0",
|
||||
"shadycss": "^1.1.0",
|
||||
"vaadin-combo-box": "vaadin/vaadin-combo-box#^3.0.2",
|
||||
"vaadin-date-picker": "vaadin/vaadin-date-picker#^2.0.0",
|
||||
"web-animations-js": "^2.2.5",
|
||||
"webcomponentsjs": "^1.0.10"
|
||||
"webcomponentsjs": "^1.0.10",
|
||||
"chart.js": "~2.7.2",
|
||||
"moment": "^2.20.0",
|
||||
"chartjs-chart-timeline": "fanthos/chartjs-chart-timeline#^0.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"web-component-tester": "^6.3.0"
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 226 KiB |
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-restricted-syntax": 0
|
||||
"import/no-extraneous-dependencies": 0,
|
||||
"no-restricted-syntax": 0,
|
||||
"no-console": 0
|
||||
}
|
||||
}
|
||||
|
10
gulp/common/gulp-uglify.js
Normal file
10
gulp/common/gulp-uglify.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* UglifyJS gulp plugin that takes in a boolean to use ES or JS minification.
|
||||
*/
|
||||
const composer = require('gulp-uglify/composer');
|
||||
const uglifyjs = require('uglify-js');
|
||||
const uglifyes = require('uglify-es');
|
||||
|
||||
module.exports = function gulpUglify(es6, options) {
|
||||
return composer(es6 ? uglifyes : uglifyjs, console)(options);
|
||||
};
|
@@ -1,58 +0,0 @@
|
||||
const {
|
||||
Analyzer,
|
||||
FSUrlLoader
|
||||
} = require('polymer-analyzer');
|
||||
|
||||
const Bundler = require('polymer-bundler').Bundler;
|
||||
const parse5 = require('parse5');
|
||||
|
||||
const { streamFromString } = require('./stream');
|
||||
|
||||
// Bundle an HTML file and convert it to a stream
|
||||
async function bundledStreamFromHTML(path, bundlerOptions = {}) {
|
||||
const bundler = new Bundler(bundlerOptions);
|
||||
const manifest = await bundler.generateManifest([path]);
|
||||
const result = await bundler.bundle(manifest);
|
||||
return streamFromString(
|
||||
path, parse5.serialize(result.documents.get(path).ast));
|
||||
}
|
||||
|
||||
async function analyze(root, paths) {
|
||||
const analyzer = new Analyzer({
|
||||
urlLoader: new FSUrlLoader(root),
|
||||
});
|
||||
return analyzer.analyze(paths);
|
||||
}
|
||||
|
||||
async function findDependencies(root, element) {
|
||||
const deps = new Set();
|
||||
|
||||
async function resolve(files) {
|
||||
const analysis = await analyze(root, files);
|
||||
const toResolve = [];
|
||||
|
||||
for (const file of files) {
|
||||
const doc = analysis.getDocument(file);
|
||||
|
||||
for (const importEl of doc.getFeatures({ kind: 'import' })) {
|
||||
const url = importEl.url;
|
||||
if (!deps.has(url)) {
|
||||
deps.add(url);
|
||||
toResolve.push(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toResolve.length > 0) {
|
||||
return resolve(toResolve);
|
||||
}
|
||||
}
|
||||
|
||||
await resolve([element]);
|
||||
return deps;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
bundledStreamFromHTML,
|
||||
findDependencies,
|
||||
};
|
8
gulp/common/md5.js
Normal file
8
gulp/common/md5.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
|
||||
module.exports = function md5(filename) {
|
||||
return crypto.createHash('md5')
|
||||
.update(fs.readFileSync(filename)).digest('hex');
|
||||
};
|
||||
|
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Polymer build strategy to strip imports, even if explictely imported
|
||||
*/
|
||||
module.exports.stripImportsStrategy = function (urls) {
|
||||
return (bundles) => {
|
||||
for (const bundle of bundles) {
|
||||
for (const url of urls) {
|
||||
bundle.stripImports.add(url);
|
||||
}
|
||||
}
|
||||
return bundles;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Polymer build strategy to strip everything but the entrypoints
|
||||
* for bundles that match a specific entry point.
|
||||
*/
|
||||
module.exports.stripAllButEntrypointStrategy = function (entryPoint) {
|
||||
return (bundles) => {
|
||||
for (const bundle of bundles) {
|
||||
if (bundle.entrypoints.size === 1 &&
|
||||
bundle.entrypoints.has(entryPoint)) {
|
||||
for (const file of bundle.files) {
|
||||
if (!bundle.entrypoints.has(file)) {
|
||||
bundle.stripImports.add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return bundles;
|
||||
};
|
||||
};
|
@@ -1,21 +0,0 @@
|
||||
const stream = require('stream');
|
||||
|
||||
const gutil = require('gulp-util');
|
||||
|
||||
function streamFromString(filename, string) {
|
||||
var src = stream.Readable({ objectMode: true });
|
||||
src._read = function () {
|
||||
this.push(new gutil.File({
|
||||
cwd: '',
|
||||
base: '',
|
||||
path: filename,
|
||||
contents: new Buffer(string)
|
||||
}));
|
||||
this.push(null);
|
||||
};
|
||||
return src;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
streamFromString,
|
||||
};
|
@@ -1,27 +1,32 @@
|
||||
const gulpif = require('gulp-if');
|
||||
|
||||
const babel = require('gulp-babel');
|
||||
const uglify = require('gulp-uglify');
|
||||
const { gulp: cssSlam } = require('css-slam');
|
||||
const htmlMinifier = require('gulp-html-minifier');
|
||||
const { HtmlSplitter } = require('polymer-build');
|
||||
const pump = require('pump');
|
||||
|
||||
module.exports.minifyStream = function (stream) {
|
||||
const uglify = require('./gulp-uglify.js');
|
||||
|
||||
module.exports.minifyStream = function (stream, es6) {
|
||||
const sourcesHtmlSplitter = new HtmlSplitter();
|
||||
return stream
|
||||
.pipe(sourcesHtmlSplitter.split())
|
||||
.pipe(gulpif(/[^app]\.js$/, babel({
|
||||
return pump([
|
||||
stream,
|
||||
sourcesHtmlSplitter.split(),
|
||||
gulpif(!es6, gulpif(/[^app]\.js$/, babel({
|
||||
sourceType: 'script',
|
||||
presets: [
|
||||
['es2015', { modules: false }]
|
||||
]
|
||||
})))
|
||||
.pipe(gulpif(/\.js$/, uglify({ sourceMap: false })))
|
||||
.pipe(gulpif(/\.css$/, cssSlam()))
|
||||
.pipe(gulpif(/\.html$/, cssSlam()))
|
||||
.pipe(gulpif(/\.html$/, htmlMinifier({
|
||||
}))),
|
||||
gulpif(/\.js$/, uglify(es6, { sourceMap: false })),
|
||||
gulpif(/\.css$/, cssSlam()),
|
||||
gulpif(/\.html$/, cssSlam()),
|
||||
gulpif(/\.html$/, htmlMinifier({
|
||||
collapseWhitespace: true,
|
||||
removeComments: true
|
||||
})))
|
||||
.pipe(sourcesHtmlSplitter.rejoin());
|
||||
})),
|
||||
sourcesHtmlSplitter.rejoin(),
|
||||
], (error) => {
|
||||
if (error) console.log(error);
|
||||
});
|
||||
};
|
||||
|
@@ -1,7 +1,8 @@
|
||||
var path = require('path');
|
||||
|
||||
module.exports = {
|
||||
static_dir: path.resolve(__dirname, '../..'),
|
||||
polymer_dir: path.resolve(__dirname, '..'),
|
||||
build_dir: path.resolve(__dirname, '../build'),
|
||||
output: path.resolve(__dirname, '../hass_frontend'),
|
||||
output_es5: path.resolve(__dirname, '../hass_frontend_es5'),
|
||||
};
|
||||
|
@@ -1,77 +0,0 @@
|
||||
self.addEventListener("push", function(event) {
|
||||
var data;
|
||||
if (event.data) {
|
||||
data = event.data.json();
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(data.title, data)
|
||||
.then(function(notification){
|
||||
firePushCallback({
|
||||
type: "received",
|
||||
tag: data.tag,
|
||||
data: data.data
|
||||
}, data.data.jwt);
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
self.addEventListener('notificationclick', function(event) {
|
||||
var url;
|
||||
|
||||
notificationEventCallback('clicked', event);
|
||||
|
||||
event.notification.close();
|
||||
|
||||
if (!event.notification.data || !event.notification.data.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
url = event.notification.data.url;
|
||||
|
||||
if (!url) return;
|
||||
|
||||
event.waitUntil(
|
||||
clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
.then(function (windowClients) {
|
||||
var i;
|
||||
var client;
|
||||
for (i = 0; i < windowClients.length; i++) {
|
||||
client = windowClients[i];
|
||||
if (client.url === url && 'focus' in client) {
|
||||
return client.focus();
|
||||
}
|
||||
}
|
||||
if (clients.openWindow) {
|
||||
return clients.openWindow(url);
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
);
|
||||
});
|
||||
self.addEventListener('notificationclose', function(event) {
|
||||
notificationEventCallback('closed', event);
|
||||
});
|
||||
|
||||
function notificationEventCallback(event_type, event){
|
||||
firePushCallback({
|
||||
action: event.action,
|
||||
data: event.notification.data,
|
||||
tag: event.notification.tag,
|
||||
type: event_type
|
||||
}, event.notification.data.jwt);
|
||||
}
|
||||
function firePushCallback(payload, jwt){
|
||||
// Don't send the JWT in the payload.data
|
||||
delete payload.data.jwt;
|
||||
// If payload.data is empty then just remove the entire payload.data object.
|
||||
if (Object.keys(payload.data).length === 0 && payload.data.constructor === Object) {
|
||||
delete payload.data;
|
||||
}
|
||||
fetch('/api/notify.html5/callback', {
|
||||
method: 'POST',
|
||||
headers: new Headers({'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer '+jwt}),
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
@@ -1,58 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
const filter = require('gulp-filter');
|
||||
const { PolymerProject, } = require('polymer-build');
|
||||
const {
|
||||
composeStrategies,
|
||||
generateShellMergeStrategy,
|
||||
} = require('polymer-bundler');
|
||||
const mergeStream = require('merge-stream');
|
||||
const rename = require('gulp-rename');
|
||||
|
||||
const polymerConfig = require('../../polymer');
|
||||
|
||||
const minifyStream = require('../common/transform').minifyStream;
|
||||
const {
|
||||
stripImportsStrategy,
|
||||
stripAllButEntrypointStrategy
|
||||
} = require('../common/strategy');
|
||||
|
||||
function renamePanel(path) {
|
||||
// Rename panels to be panels/* and not their subdir
|
||||
if (path.basename.substr(0, 9) === 'ha-panel-' && path.extname === '.html') {
|
||||
path.dirname = 'panels/';
|
||||
}
|
||||
|
||||
// Rename frontend
|
||||
if (path.dirname === 'src' && path.basename === 'home-assistant' &&
|
||||
path.extname === '.html') {
|
||||
path.dirname = '';
|
||||
path.basename = 'frontend';
|
||||
}
|
||||
}
|
||||
|
||||
gulp.task('build', ['ru_all', 'build-translations'], () => {
|
||||
const strategy = composeStrategies([
|
||||
generateShellMergeStrategy(polymerConfig.shell),
|
||||
stripImportsStrategy([
|
||||
'bower_components/font-roboto/roboto.html',
|
||||
'bower_components/paper-styles/color.html',
|
||||
]),
|
||||
stripAllButEntrypointStrategy('panels/hassio/ha-panel-hassio.html')
|
||||
]);
|
||||
const project = new PolymerProject(polymerConfig);
|
||||
|
||||
return mergeStream(minifyStream(project.sources()),
|
||||
minifyStream(project.dependencies()))
|
||||
.pipe(project.bundler({
|
||||
strategy,
|
||||
strip: true,
|
||||
sourcemaps: false,
|
||||
stripComments: true,
|
||||
inlineScripts: true,
|
||||
inlineCss: true,
|
||||
implicitStrip: true,
|
||||
}))
|
||||
.pipe(rename(renamePanel))
|
||||
.pipe(filter(['**', '!src/entrypoint.html']))
|
||||
.pipe(gulp.dest('build/'));
|
||||
});
|
@@ -1,6 +0,0 @@
|
||||
const del = require('del');
|
||||
const gulp = require('gulp');
|
||||
|
||||
gulp.task('clean', () => {
|
||||
return del(['build', 'build-temp']);
|
||||
});
|
@@ -1,9 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
const runSequence = require('run-sequence');
|
||||
|
||||
gulp.task('default', () => {
|
||||
return runSequence.use(gulp)(
|
||||
'clean',
|
||||
'build'
|
||||
);
|
||||
});
|
43
gulp/tasks/gen-authorize-html.js
Normal file
43
gulp/tasks/gen-authorize-html.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const gulp = require('gulp');
|
||||
const path = require('path');
|
||||
const replace = require('gulp-batch-replace');
|
||||
const rename = require('gulp-rename');
|
||||
const md5 = require('../common/md5');
|
||||
const url = require('url');
|
||||
|
||||
const config = require('../config');
|
||||
const minifyStream = require('../common/transform').minifyStream;
|
||||
|
||||
const buildReplaces = {
|
||||
'/frontend_latest/authorize.js': 'authorize.js',
|
||||
};
|
||||
|
||||
const es5Extra = "<script src='/static/custom-elements-es5-adapter.js'></script>";
|
||||
|
||||
async function buildAuth(es6) {
|
||||
const targetPath = es6 ? config.output : config.output_es5;
|
||||
const targetUrl = es6 ? '/frontend_latest/' : '/frontend_es5/';
|
||||
const frontendPath = es6 ? 'frontend_latest' : 'frontend_es5';
|
||||
const toReplace = [
|
||||
['<!--EXTRA_SCRIPTS-->', es6 ? '' : es5Extra],
|
||||
['/home-assistant-polymer/hass_frontend/authorize.js', `/${frontendPath}/authorize.js`],
|
||||
];
|
||||
|
||||
for (const [replaceSearch, filename] of Object.entries(buildReplaces)) {
|
||||
const parsed = path.parse(filename);
|
||||
const hash = md5(path.resolve(targetPath, filename));
|
||||
toReplace.push([
|
||||
replaceSearch,
|
||||
url.resolve(targetUrl, `${parsed.name}-${hash}${parsed.ext}`)]);
|
||||
}
|
||||
|
||||
const stream = gulp.src(path.resolve(config.polymer_dir, 'src/authorize.html'))
|
||||
.pipe(replace(toReplace));
|
||||
|
||||
return minifyStream(stream, /* es6= */ es6)
|
||||
.pipe(rename('authorize.html'))
|
||||
.pipe(gulp.dest(es6 ? config.output : config.output_es5));
|
||||
}
|
||||
|
||||
gulp.task('gen-authorize-html-es5', () => buildAuth(/* es6= */ false));
|
||||
gulp.task('gen-authorize-html', () => buildAuth(/* es6= */ true));
|
107
gulp/tasks/gen-icons.js
Normal file
107
gulp/tasks/gen-icons.js
Normal file
@@ -0,0 +1,107 @@
|
||||
const gulp = require('gulp');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const config = require('../config');
|
||||
|
||||
const ICON_PACKAGE_PATH = path.resolve(__dirname, '../../node_modules/@mdi/svg/');
|
||||
const META_PATH = path.resolve(ICON_PACKAGE_PATH, 'meta.json');
|
||||
const ICON_PATH = path.resolve(ICON_PACKAGE_PATH, 'svg');
|
||||
const OUTPUT_DIR = path.resolve(__dirname, '../../build');
|
||||
const MDI_OUTPUT_PATH = path.resolve(OUTPUT_DIR, 'mdi.html');
|
||||
const HASS_OUTPUT_PATH = path.resolve(OUTPUT_DIR, 'hass-icons.html');
|
||||
|
||||
const BUILT_IN_PANEL_ICONS = [
|
||||
'calendar', // Calendar
|
||||
'settings', // Config
|
||||
'home-assistant', // Hass.io
|
||||
'poll-box', // History panel
|
||||
'format-list-bulleted-type', // Logbook
|
||||
'mailbox', // Mailbox
|
||||
'account-location', // Map
|
||||
'cart', // Shopping List
|
||||
];
|
||||
|
||||
// Given an icon name, load the SVG file
|
||||
function loadIcon(name) {
|
||||
const iconPath = path.resolve(ICON_PATH, `${name}.svg`);
|
||||
try {
|
||||
return fs.readFileSync(iconPath, 'utf-8');
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Given an SVG file, convert it to an iron-iconset-svg definition
|
||||
function transformXMLtoPolymer(name, xml) {
|
||||
const start = xml.indexOf('><path') + 1;
|
||||
const end = xml.length - start - 6;
|
||||
const path = xml.substr(start, end);
|
||||
return `<g id="${name}">${path}</g>`;
|
||||
}
|
||||
|
||||
// Given an iconset name and icon names, generate a polymer iconset
|
||||
function generateIconset(name, iconNames) {
|
||||
const iconDefs = iconNames.map(name => {
|
||||
const iconDef = loadIcon(name);
|
||||
if (!iconDef) {
|
||||
throw new Error(`Unknown icon referenced: ${name}`);
|
||||
}
|
||||
return transformXMLtoPolymer(name, iconDef)
|
||||
}).join('');
|
||||
return `<ha-iconset-svg name="${name}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`;
|
||||
}
|
||||
|
||||
// Generate the full MDI iconset
|
||||
function genMDIIcons() {
|
||||
const meta = JSON.parse(fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), 'UTF-8'));
|
||||
const iconNames = meta.map(iconInfo => iconInfo.name);
|
||||
fs.existsSync(OUTPUT_DIR) || fs.mkdirSync(OUTPUT_DIR);
|
||||
fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset('mdi', iconNames));
|
||||
}
|
||||
|
||||
// Helper function to map recursively over files in a folder and it's subfolders
|
||||
function mapFiles(startPath, filter, mapFunc) {
|
||||
const files = fs.readdirSync(startPath);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const filename = path.join(startPath, files[i]);
|
||||
const stat = fs.lstatSync(filename);
|
||||
if (stat.isDirectory()) {
|
||||
mapFiles(filename, filter, mapFunc);
|
||||
} else if (filename.indexOf(filter) >= 0) {
|
||||
mapFunc(filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find all icons used by the project.
|
||||
function findIcons(path, iconsetName) {
|
||||
const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, 'g');
|
||||
const icons = new Set();
|
||||
function processFile(filename) {
|
||||
const content = fs.readFileSync(filename);
|
||||
let match;
|
||||
// eslint-disable-next-line
|
||||
while (match = iconRegex.exec(content)) {
|
||||
// strip off "hass:" and add to set
|
||||
icons.add(match[0].substr(iconsetName.length + 1));
|
||||
}
|
||||
}
|
||||
mapFiles(path, '.js', processFile);
|
||||
return Array.from(icons);
|
||||
}
|
||||
|
||||
function genHassIcons() {
|
||||
const iconNames = findIcons('./src', 'hass').concat(BUILT_IN_PANEL_ICONS);
|
||||
fs.existsSync(OUTPUT_DIR) || fs.mkdirSync(OUTPUT_DIR);
|
||||
fs.writeFileSync(HASS_OUTPUT_PATH, generateIconset('hass', iconNames));
|
||||
}
|
||||
|
||||
gulp.task('gen-icons-mdi', () => genMDIIcons());
|
||||
gulp.task('gen-icons-hass', () => genHassIcons());
|
||||
gulp.task('gen-icons-hassio', () => genHassIcons());
|
||||
gulp.task('gen-icons', ['gen-icons-hass', 'gen-icons-mdi'], () => {});
|
||||
|
||||
module.exports = {
|
||||
findIcons,
|
||||
generateIconset,
|
||||
};
|
54
gulp/tasks/gen-index-html.js
Normal file
54
gulp/tasks/gen-index-html.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const gulp = require('gulp');
|
||||
const replace = require('gulp-batch-replace');
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
const config = require('../config');
|
||||
const md5 = require('../common/md5');
|
||||
const { minifyStream } = require('../common/transform');
|
||||
|
||||
const buildReplaces = {
|
||||
'/frontend_latest/core.js': 'core.js',
|
||||
'/frontend_latest/app.js': 'app.js',
|
||||
};
|
||||
|
||||
function generateIndex(es6) {
|
||||
const targetPath = es6 ? config.output : config.output_es5;
|
||||
const targetUrl = es6 ? '/frontend_latest/' : '/frontend_es5/';
|
||||
|
||||
const toReplace = [
|
||||
// Needs to look like a color during CSS minifiaction
|
||||
['{{ theme_color }}', '#THEME'],
|
||||
['/frontend_latest/hass-icons.js',
|
||||
`/frontend_latest/hass-icons-${md5(path.resolve(config.output, 'hass-icons.js'))}.js`],
|
||||
];
|
||||
|
||||
if (!es6) {
|
||||
const compatibilityPath = `/frontend_es5/compatibility-${md5(path.resolve(config.output_es5, 'compatibility.js'))}.js`;
|
||||
const es5Extra = `
|
||||
<script src='${compatibilityPath}'></script>
|
||||
<script src='/static/custom-elements-es5-adapter.js'></script>
|
||||
`;
|
||||
|
||||
toReplace.push([
|
||||
'<!--EXTRA_SCRIPTS-->', es5Extra
|
||||
]);
|
||||
}
|
||||
|
||||
for (const [replaceSearch, filename] of Object.entries(buildReplaces)) {
|
||||
const parsed = path.parse(filename);
|
||||
const hash = md5(path.resolve(targetPath, filename));
|
||||
toReplace.push([
|
||||
replaceSearch,
|
||||
url.resolve(targetUrl, `${parsed.name}-${hash}${parsed.ext}`)]);
|
||||
}
|
||||
|
||||
const stream = gulp.src(path.resolve(config.polymer_dir, 'index.html'))
|
||||
.pipe(replace(toReplace));
|
||||
|
||||
return minifyStream(stream, es6)
|
||||
.pipe(replace([['#THEME', '{{ theme_color }}']]))
|
||||
.pipe(gulp.dest(targetPath));
|
||||
}
|
||||
|
||||
gulp.task('gen-index-html-es5', generateIndex.bind(null, /* es6= */ false));
|
||||
gulp.task('gen-index-html', generateIndex.bind(null, /* es6= */ true));
|
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
Generate a caching service worker for HA
|
||||
|
||||
Will be called as part of build_frontend.
|
||||
|
||||
Expects home-assistant-polymer repo as submodule of HA repo.
|
||||
Creates a caching service worker based on the CURRENT content of HA repo.
|
||||
Output service worker to build/service_worker.js
|
||||
|
||||
TODO:
|
||||
- Use gulp streams
|
||||
- Fix minifying the stream
|
||||
*/
|
||||
var gulp = require('gulp');
|
||||
var crypto = require('crypto');
|
||||
var file = require('gulp-file');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var swPrecache = require('sw-precache');
|
||||
var uglifyJS = require('uglify-js');
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
const DEV = !!JSON.parse(process.env.BUILD_DEV || 'true');
|
||||
|
||||
var rootDir = 'hass_frontend';
|
||||
var panelDir = path.resolve(rootDir, 'panels');
|
||||
|
||||
var dynamicUrlToDependencies = {};
|
||||
|
||||
var staticFingerprinted = [
|
||||
'frontend.html',
|
||||
'mdi.html',
|
||||
'core.js',
|
||||
'compatibility.js',
|
||||
'translations/en.json',
|
||||
];
|
||||
|
||||
// These panels will always be registered inside HA and thus can
|
||||
// be safely assumed to be able to preload.
|
||||
var panelsFingerprinted = [
|
||||
'dev-event', 'dev-info', 'dev-service', 'dev-state', 'dev-template',
|
||||
'dev-mqtt', 'kiosk',
|
||||
];
|
||||
|
||||
function md5(filename) {
|
||||
return crypto.createHash('md5')
|
||||
.update(fs.readFileSync(filename)).digest('hex');
|
||||
}
|
||||
|
||||
gulp.task('gen-service-worker', () => {
|
||||
var genPromise = null;
|
||||
if (DEV) {
|
||||
var devBase = 'console.warn("Service worker caching disabled in development")';
|
||||
genPromise = Promise.resolve(devBase);
|
||||
} else {
|
||||
// Create fingerprinted versions of our dependencies.
|
||||
staticFingerprinted.forEach(fn => {
|
||||
var parts = path.parse(fn);
|
||||
var base = parts.dir.length > 0 ? parts.dir + '/' + parts.name : parts.name;
|
||||
var hash = md5(rootDir + '/' + base + parts.ext);
|
||||
var url = '/static/' + base + '-' + hash + parts.ext;
|
||||
var fpath = rootDir + '/' + base + parts.ext;
|
||||
dynamicUrlToDependencies[url] = [fpath];
|
||||
});
|
||||
|
||||
panelsFingerprinted.forEach(panel => {
|
||||
var fpath = panelDir + '/ha-panel-' + panel + '.html';
|
||||
var hash = md5(fpath);
|
||||
var url = '/static/panels/ha-panel-' + panel + '-' + hash + '.html';
|
||||
dynamicUrlToDependencies[url] = [fpath];
|
||||
});
|
||||
var fallbackList = '(?!(?:static|api|local|service_worker.js|manifest.json))';
|
||||
|
||||
var options = {
|
||||
navigateFallback: '/',
|
||||
navigateFallbackWhitelist: [RegExp('^(?:' + fallbackList + '.)*$')],
|
||||
dynamicUrlToDependencies: dynamicUrlToDependencies,
|
||||
staticFileGlobs: [
|
||||
rootDir + '/icons/favicon.ico',
|
||||
rootDir + '/icons/favicon-192x192.png',
|
||||
rootDir + '/webcomponents-lite.min.js',
|
||||
rootDir + '/fonts/roboto/Roboto-Light.ttf',
|
||||
rootDir + '/fonts/roboto/Roboto-Medium.ttf',
|
||||
rootDir + '/fonts/roboto/Roboto-Regular.ttf',
|
||||
rootDir + '/fonts/roboto/Roboto-Bold.ttf',
|
||||
rootDir + '/images/card_media_player_bg.png',
|
||||
],
|
||||
runtimeCaching: [{
|
||||
urlPattern: /\/static\/translations\//,
|
||||
handler: 'cacheFirst',
|
||||
}, {
|
||||
urlPattern: RegExp('^[^/]*/' + fallbackList + '.'),
|
||||
handler: 'fastest',
|
||||
}],
|
||||
stripPrefix: 'hass_frontend',
|
||||
replacePrefix: 'static',
|
||||
verbose: true,
|
||||
// Allow our users to refresh to get latest version.
|
||||
clientsClaim: true,
|
||||
};
|
||||
|
||||
genPromise = swPrecache.generate(options);
|
||||
}
|
||||
|
||||
var swHass = fs.readFileSync(path.resolve(__dirname, '../service-worker.js.tmpl'), 'UTF-8');
|
||||
|
||||
// Fix this
|
||||
// if (!DEV) {
|
||||
// genPromise = genPromise.then(
|
||||
// swString => uglifyJS.minify(swString, { fromString: true }).code);
|
||||
// }
|
||||
|
||||
return genPromise.then(swString => swString + '\n' + swHass)
|
||||
.then(swString => file('service_worker.js', swString)
|
||||
.pipe(gulp.dest(config.build_dir)));
|
||||
});
|
@@ -1,47 +0,0 @@
|
||||
var gulp = require('gulp');
|
||||
const rename = require('gulp-rename');
|
||||
|
||||
const {
|
||||
stripImportsStrategy,
|
||||
} = require('../common/strategy');
|
||||
const minifyStream = require('../common/transform').minifyStream;
|
||||
const {
|
||||
bundledStreamFromHTML,
|
||||
findDependencies
|
||||
} = require('../common/html');
|
||||
|
||||
const { polymer_dir } = require('../config');
|
||||
|
||||
const DEPS_TO_STRIP = [
|
||||
'bower_components/font-roboto/roboto.html',
|
||||
'bower_components/paper-styles/color.html',
|
||||
'bower_components/iron-meta/iron-meta.html',
|
||||
];
|
||||
const DEPS_TO_STRIP_RECURSIVELY = [
|
||||
'bower_components/polymer/polymer.html',
|
||||
];
|
||||
|
||||
gulp.task(
|
||||
'hassio-panel',
|
||||
async () => {
|
||||
const toStrip = [...DEPS_TO_STRIP];
|
||||
|
||||
for (let dep of DEPS_TO_STRIP_RECURSIVELY) {
|
||||
toStrip.push(dep);
|
||||
const deps = await findDependencies(polymer_dir, dep);
|
||||
for (const importUrl of deps) {
|
||||
toStrip.push(importUrl);
|
||||
}
|
||||
}
|
||||
|
||||
const stream = await bundledStreamFromHTML(
|
||||
'panels/hassio/hassio-main.html', {
|
||||
strategy: stripImportsStrategy(toStrip)
|
||||
}
|
||||
);
|
||||
|
||||
return minifyStream(stream)
|
||||
.pipe(rename('hassio-main.html'))
|
||||
.pipe(gulp.dest('build-temp/'));
|
||||
}
|
||||
);
|
@@ -1,30 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
const rollupEach = require('gulp-rollup-each');
|
||||
const rollupConfig = require('../../rollup.config');
|
||||
|
||||
gulp.task('run_rollup', () => {
|
||||
return gulp.src([
|
||||
'js/core.js',
|
||||
'js/compatibility.js',
|
||||
'js/automation-editor/automation-editor.js',
|
||||
'js/script-editor/script-editor.js',
|
||||
'demo_data/demo_data.js',
|
||||
])
|
||||
.pipe(rollupEach(rollupConfig, rollupConfig))
|
||||
.pipe(gulp.dest('build-temp'));
|
||||
});
|
||||
|
||||
gulp.task('ru_all', ['run_rollup'], () => {
|
||||
gulp.src([
|
||||
'build-temp/core.js',
|
||||
'build-temp/compatibility.js',
|
||||
])
|
||||
.pipe(gulp.dest('build/'));
|
||||
});
|
||||
|
||||
gulp.task('watch_ru_all', ['ru_all'], () => {
|
||||
gulp.watch([
|
||||
'js/**/*.js',
|
||||
'demo_data/**/*.js'
|
||||
], ['ru_all']);
|
||||
});
|
@@ -8,76 +8,192 @@ const minify = require('gulp-jsonminify');
|
||||
const rename = require('gulp-rename');
|
||||
const transform = require('gulp-json-transform');
|
||||
|
||||
const inDir = 'translations'
|
||||
const outDir = 'build/translations';
|
||||
const inDir = 'translations';
|
||||
const workDir = 'build-translations';
|
||||
const fullDir = workDir + '/full';
|
||||
const coreDir = workDir + '/core';
|
||||
const outDir = workDir + '/output';
|
||||
|
||||
// Panel translations which should be split from the core translations. These
|
||||
// should mirror the fragment definitions in polymer.json, so that we load
|
||||
// additional resources at equivalent points.
|
||||
const TRANSLATION_FRAGMENTS = [
|
||||
'config',
|
||||
'history',
|
||||
'logbook',
|
||||
'mailbox',
|
||||
'shopping-list',
|
||||
];
|
||||
|
||||
const tasks = [];
|
||||
|
||||
function recursive_flatten (prefix, data) {
|
||||
var output = {};
|
||||
function recursiveFlatten(prefix, data) {
|
||||
let output = {};
|
||||
Object.keys(data).forEach(function (key) {
|
||||
if (typeof(data[key]) === 'object') {
|
||||
output = Object.assign({}, output, recursive_flatten(key + '.', data[key]));
|
||||
if (typeof (data[key]) === 'object') {
|
||||
output = Object.assign({}, output, recursiveFlatten(prefix + key + '.', data[key]));
|
||||
} else {
|
||||
output[prefix + key] = data[key];
|
||||
}
|
||||
});
|
||||
return output
|
||||
return output;
|
||||
}
|
||||
|
||||
function flatten (data) {
|
||||
return recursive_flatten('', data);
|
||||
function flatten(data) {
|
||||
return recursiveFlatten('', data);
|
||||
}
|
||||
|
||||
var taskName = 'build-translation-native-names';
|
||||
gulp.task(taskName, function() {
|
||||
return gulp.src(inDir + '/*.json')
|
||||
function emptyFilter(data) {
|
||||
const newData = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (data[key]) {
|
||||
if (typeof (data[key]) === 'object') {
|
||||
newData[key] = emptyFilter(data[key]);
|
||||
} else {
|
||||
newData[key] = data[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
return newData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace Lokalise key placeholders with their actual values.
|
||||
*
|
||||
* We duplicate the behavior of Lokalise here so that placeholders can
|
||||
* be included in src/translations/en.json, but still be usable while
|
||||
* developing locally.
|
||||
*
|
||||
* @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing
|
||||
*/
|
||||
const re_key_reference = /\[%key:([^%]+)%\]/;
|
||||
function lokalise_transform (data, original) {
|
||||
const output = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value instanceof Object) {
|
||||
output[key] = lokalise_transform(value, original);
|
||||
} else {
|
||||
output[key] = value.replace(re_key_reference, (match, key) => {
|
||||
const replace = key.split('::').reduce((tr, k) => tr[k], original);
|
||||
if (typeof replace !== 'string') {
|
||||
throw Error(`Invalid key placeholder ${key} in src/translations/en.json`);
|
||||
}
|
||||
return replace;
|
||||
});
|
||||
}
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* This task will build a master translation file, to be used as the base for
|
||||
* all languages. This starts with src/translations/en.json, and replaces all
|
||||
* Lokalise key placeholders with their target values. Under normal circumstances,
|
||||
* this will be the same as translations/en.json However, we build it here to
|
||||
* facilitate both making changes in development mode, and to ensure that the
|
||||
* project is buildable immediately after merging new translation keys, since
|
||||
* the Lokalise update to translations/en.json will not happen immediately.
|
||||
*/
|
||||
let taskName = 'build-master-translation';
|
||||
gulp.task(taskName, function () {
|
||||
return gulp.src('src/translations/en.json')
|
||||
.pipe(transform(function(data, file) {
|
||||
// Look up the native name for each language and generate a json
|
||||
// object with all available languages and native names
|
||||
const lang = path.basename(file.relative, '.json');
|
||||
return {[lang]: {nativeName: data.language[lang]}};
|
||||
return lokalise_transform(data, data);
|
||||
}))
|
||||
.pipe(merge({
|
||||
fileName: 'translationNativeNames.json',
|
||||
}))
|
||||
.pipe(gulp.dest('build-temp'));
|
||||
.pipe(rename('translationMaster.json'))
|
||||
.pipe(gulp.dest(workDir));
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
var taskName = 'build-merged-translations';
|
||||
gulp.task(taskName, function () {
|
||||
taskName = 'build-merged-translations';
|
||||
gulp.task(taskName, ['build-master-translation'], function () {
|
||||
return gulp.src(inDir + '/*.json')
|
||||
.pipe(foreach(function(stream, file) {
|
||||
// For each language generate a merged json file. It begins with en.json as
|
||||
// a failsafe for untranslated strings, and merges all parent tags into one
|
||||
// file for each specific subtag
|
||||
// For each language generate a merged json file. It begins with the master
|
||||
// translation as a failsafe for untranslated strings, and merges all parent
|
||||
// tags into one file for each specific subtag
|
||||
//
|
||||
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
||||
// Will be OK for now as long as we don't have anything more complicated
|
||||
// than a base translation + region.
|
||||
const tr = path.basename(file.history[0], '.json');
|
||||
const subtags = tr.split('-');
|
||||
const src = [inDir + '/en.json']; // Start with en as a fallback for missing translations
|
||||
for (i = 1; i <= subtags.length; i++) {
|
||||
const src = [workDir + '/translationMaster.json'];
|
||||
for (let i = 1; i <= subtags.length; i++) {
|
||||
const lang = subtags.slice(0, i).join('-');
|
||||
src.push(inDir + '/' + lang + '.json');
|
||||
}
|
||||
return gulp.src(src)
|
||||
.pipe(transform(data => emptyFilter(data)))
|
||||
.pipe(merge({
|
||||
fileName: tr + '.json',
|
||||
}))
|
||||
.pipe(transform(function(data, file) {
|
||||
// Polymer.AppLocalizeBehavior requires flattened json
|
||||
return flatten(data);
|
||||
}))
|
||||
.pipe(minify())
|
||||
.pipe(gulp.dest(outDir));
|
||||
.pipe(gulp.dest(fullDir));
|
||||
}));
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
var taskName = 'build-translation-fingerprints';
|
||||
gulp.task(taskName, ['build-merged-translations'], function() {
|
||||
return gulp.src(outDir + '/*.json')
|
||||
const splitTasks = [];
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
taskName = 'build-translation-fragment-' + fragment;
|
||||
gulp.task(taskName, ['build-merged-translations'], function () {
|
||||
// Return only the translations for this fragment.
|
||||
return gulp.src(fullDir + '/*.json')
|
||||
.pipe(transform(data => ({
|
||||
ui: {
|
||||
panel: {
|
||||
[fragment]: data.ui.panel[fragment],
|
||||
},
|
||||
},
|
||||
})))
|
||||
.pipe(gulp.dest(workDir + '/' + fragment));
|
||||
});
|
||||
tasks.push(taskName);
|
||||
splitTasks.push(taskName);
|
||||
});
|
||||
|
||||
taskName = 'build-translation-core';
|
||||
gulp.task(taskName, ['build-merged-translations'], function () {
|
||||
// Remove the fragment translations from the core translation.
|
||||
return gulp.src(fullDir + '/*.json')
|
||||
.pipe(transform((data) => {
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
delete data.ui.panel[fragment];
|
||||
});
|
||||
return data;
|
||||
}))
|
||||
.pipe(gulp.dest(coreDir));
|
||||
});
|
||||
tasks.push(taskName);
|
||||
splitTasks.push(taskName);
|
||||
|
||||
taskName = 'build-flattened-translations';
|
||||
gulp.task(taskName, splitTasks, function () {
|
||||
// Flatten the split versions of our translations, and move them into outDir
|
||||
return gulp.src(
|
||||
TRANSLATION_FRAGMENTS.map(fragment => workDir + '/' + fragment + '/*.json')
|
||||
.concat(coreDir + '/*.json'),
|
||||
{ base: workDir },
|
||||
)
|
||||
.pipe(transform(function (data) {
|
||||
// Polymer.AppLocalizeBehavior requires flattened json
|
||||
return flatten(data);
|
||||
}))
|
||||
.pipe(minify())
|
||||
.pipe(rename((filePath) => {
|
||||
if (filePath.dirname === 'core') {
|
||||
filePath.dirname = '';
|
||||
}
|
||||
}))
|
||||
.pipe(gulp.dest(outDir));
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = 'build-translation-fingerprints';
|
||||
gulp.task(taskName, ['build-flattened-translations'], function () {
|
||||
return gulp.src(outDir + '/**/*.json')
|
||||
.pipe(rename({
|
||||
extname: "",
|
||||
extname: '',
|
||||
}))
|
||||
.pipe(hash({
|
||||
algorithm: 'md5',
|
||||
@@ -85,26 +201,55 @@ gulp.task(taskName, ['build-merged-translations'], function() {
|
||||
template: '<%= name %>-<%= hash %>.json',
|
||||
}))
|
||||
.pipe(hash.manifest('translationFingerprints.json'))
|
||||
.pipe(transform(function(data, file) {
|
||||
Object.keys(data).map(function(key, index) {
|
||||
data[key] = {fingerprint: data[key]};
|
||||
.pipe(transform(function (data) {
|
||||
// After generating fingerprints of our translation files, consolidate
|
||||
// all translation fragment fingerprints under the translation name key
|
||||
const newData = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
const parts = key.split('/');
|
||||
let translation = key;
|
||||
if (parts.length === 2) {
|
||||
translation = parts[1];
|
||||
}
|
||||
if (!(translation in newData)) {
|
||||
newData[translation] = {
|
||||
fingerprints: {},
|
||||
};
|
||||
}
|
||||
newData[translation].fingerprints[key] = value;
|
||||
});
|
||||
return data;
|
||||
return newData;
|
||||
}))
|
||||
.pipe(gulp.dest('build-temp'));
|
||||
.pipe(gulp.dest(workDir));
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
var taskName = 'build-translations';
|
||||
gulp.task(taskName, ['build-translation-fingerprints', 'build-translation-native-names'], function() {
|
||||
taskName = 'build-translations';
|
||||
gulp.task(taskName, ['build-translation-fingerprints'], function () {
|
||||
return gulp.src([
|
||||
'build-temp/translationFingerprints.json',
|
||||
'build-temp/translationNativeNames.json',
|
||||
])
|
||||
'src/translations/translationMetadata.json',
|
||||
workDir + '/translationFingerprints.json',
|
||||
])
|
||||
.pipe(merge({}))
|
||||
.pipe(insert.wrap('<script>\nwindow.translationMetadata = ', ';\n</script>'))
|
||||
.pipe(rename('translationMetadata.html'))
|
||||
.pipe(gulp.dest('build-temp'));
|
||||
.pipe(transform(function (data) {
|
||||
const newData = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
// Filter out translations without native name.
|
||||
if (data[key].nativeName) {
|
||||
newData[key] = data[key];
|
||||
} else {
|
||||
console.warn(`Skipping language ${key}. Native name was not translated.`);
|
||||
}
|
||||
if (data[key]) newData[key] = value;
|
||||
});
|
||||
return newData;
|
||||
}))
|
||||
.pipe(transform(data => ({
|
||||
fragments: TRANSLATION_FRAGMENTS,
|
||||
translations: data,
|
||||
})))
|
||||
.pipe(rename('translationMetadata.json'))
|
||||
.pipe(gulp.dest(workDir));
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
|
2
hassio/.gitignore
vendored
Normal file
2
hassio/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
build-es5/*
|
||||
hassio-icons.html
|
10
hassio/config.js
Normal file
10
hassio/config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
// Target directory for the build.
|
||||
buildDirLegacy: path.resolve(__dirname, 'build-es5'),
|
||||
buildDir: path.resolve(__dirname, 'build'),
|
||||
// Path where the Hass.io frontend will be publicly available.
|
||||
publicPath: '/api/hassio/app',
|
||||
publicPathLegacy: '/api/hassio/app-es5',
|
||||
}
|
38
hassio/index.html
Normal file
38
hassio/index.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Hass.io</title>
|
||||
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
||||
<style>
|
||||
body {
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<!--EXTRA_SCRIPTS-->
|
||||
</head>
|
||||
<body>
|
||||
<hassio-app></hassio-app>
|
||||
<script>
|
||||
function addScript(src) {
|
||||
var e = document.createElement('script');
|
||||
e.src = src;
|
||||
document.write(e.outerHTML);
|
||||
}
|
||||
var webComponentsSupported = (
|
||||
'customElements' in window &&
|
||||
'import' in document.createElement('link') &&
|
||||
'content' in document.createElement('template'));
|
||||
if (!webComponentsSupported) {
|
||||
addScript('/static/webcomponents-lite.js');
|
||||
}
|
||||
</script>
|
||||
<!--
|
||||
Disabled while we make Home Assistant able to serve the right files.
|
||||
<script src="./app.js"></script>
|
||||
-->
|
||||
<link rel='import' href='./hassio-app.html'>
|
||||
</body>
|
||||
</html>
|
28
hassio/script/build_hassio
Executable file
28
hassio/script/build_hassio
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
# Builds the Hass.io app for production
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
OUTPUT_DIR=build
|
||||
OUTPUT_DIR_ES5=build-es5
|
||||
|
||||
rm -rf $OUTPUT_DIR $OUTPUT_DIR_ES5
|
||||
node script/gen-icons.js
|
||||
|
||||
# LEGACY BUILD
|
||||
NODE_ENV=production ../node_modules/.bin/webpack -p --config webpack.legacy.config.js
|
||||
node script/gen-index-html.js
|
||||
|
||||
# Temporarily re-create old HTML import
|
||||
echo "<script>" > $OUTPUT_DIR_ES5/hassio-app.html
|
||||
cat $OUTPUT_DIR_ES5/app.js >> $OUTPUT_DIR_ES5/hassio-app.html
|
||||
cat $OUTPUT_DIR_ES5/chunk.*.js >> $OUTPUT_DIR_ES5/hassio-app.html
|
||||
echo "</script>" >> $OUTPUT_DIR_ES5/hassio-app.html
|
||||
rm $OUTPUT_DIR_ES5/app.js*
|
||||
rm $OUTPUT_DIR_ES5/chunk.*
|
||||
|
||||
# NEW BUILD
|
||||
NODE_ENV=production ../node_modules/.bin/webpack -p --config webpack.config.js
|
12
hassio/script/develop
Executable file
12
hassio/script/develop
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
# Run the Hass.io development server
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
OUTPUT_DIR=build
|
||||
|
||||
rm -rf $OUTPUT_DIR
|
||||
mkdir $OUTPUT_DIR
|
||||
node script/gen-icons.js
|
||||
../node_modules/.bin/webpack --watch --progress
|
15
hassio/script/gen-icons.js
Executable file
15
hassio/script/gen-icons.js
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const {
|
||||
findIcons,
|
||||
generateIconset,
|
||||
} = require('../../gulp/tasks/gen-icons.js');
|
||||
|
||||
const MENU_BUTTON_ICON = 'menu';
|
||||
|
||||
function genHassioIcons() {
|
||||
const iconNames = findIcons('./src', 'hassio').concat(MENU_BUTTON_ICON);
|
||||
fs.writeFileSync('./hassio-icons.html', generateIconset('hassio', iconNames));
|
||||
}
|
||||
|
||||
genHassioIcons();
|
18
hassio/script/gen-index-html.js
Executable file
18
hassio/script/gen-index-html.js
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const config = require('../config.js');
|
||||
|
||||
let index = fs.readFileSync('index.html', 'utf-8');
|
||||
|
||||
const toReplace = [
|
||||
[
|
||||
'<!--EXTRA_SCRIPTS-->',
|
||||
"<script src='/frontend_es5/custom-elements-es5-adapter.js'></script>"
|
||||
],
|
||||
];
|
||||
|
||||
for (item of toReplace) {
|
||||
index = index.replace(item[0], item[1]);
|
||||
}
|
||||
|
||||
fs.writeFileSync(`${config.buildDirLegacy}/index.html`, index);
|
73
hassio/src/addon-store/hassio-addon-repository.js
Normal file
73
hassio/src/addon-store/hassio-addon-repository.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import '@polymer/paper-card/paper-card.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../components/hassio-card-content.js';
|
||||
import '../resources/hassio-style.js';
|
||||
import NavigateMixin from '../../../src/mixins/navigate-mixin.js';
|
||||
|
||||
class HassioAddonRepository extends NavigateMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style hassio-style">
|
||||
paper-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
a.repo {
|
||||
display: block;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
</style>
|
||||
<template is="dom-if" if="[[addons.length]]">
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
[[repo.name]]
|
||||
<div class="description">
|
||||
Maintained by [[repo.maintainer]]
|
||||
<a class="repo" href="[[repo.url]]" target="_blank">[[repo.url]]</a>
|
||||
</div>
|
||||
</div>
|
||||
<template is="dom-repeat" items="[[addons]]" as="addon" sort="sortAddons">
|
||||
<paper-card on-click="addonTapped">
|
||||
<div class="card-content">
|
||||
<hassio-card-content hass="[[hass]]" title="[[addon.name]]" description="[[addon.description]]" icon="[[computeIcon(addon)]]" icon-title="[[computeIconTitle(addon)]]" icon-class="[[computeIconClass(addon)]]"></hassio-card-content>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
repo: Object,
|
||||
addons: Array,
|
||||
};
|
||||
}
|
||||
|
||||
sortAddons(a, b) {
|
||||
return a.name < b.name ? -1 : 1;
|
||||
}
|
||||
|
||||
computeIcon(addon) {
|
||||
return addon.installed && addon.installed !== addon.version ? 'hassio:arrow-up-bold-circle' : 'hassio:puzzle';
|
||||
}
|
||||
|
||||
computeIconTitle(addon) {
|
||||
if (addon.installed) return addon.installed !== addon.version ? 'New version available' : 'Add-on is installed';
|
||||
return 'Add-on is not installed';
|
||||
}
|
||||
|
||||
computeIconClass(addon) {
|
||||
if (addon.installed) return addon.installed !== addon.version ? 'update' : 'installed';
|
||||
return '';
|
||||
}
|
||||
|
||||
addonTapped(ev) {
|
||||
this.navigate(`/hassio/addon/${ev.model.addon.slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-addon-repository', HassioAddonRepository);
|
82
hassio/src/addon-store/hassio-addon-store.js
Normal file
82
hassio/src/addon-store/hassio-addon-store.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import './hassio-addon-repository.js';
|
||||
import './hassio-repositories-editor.js';
|
||||
|
||||
class HassioAddonStore extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
hassio-addon-repository {
|
||||
margin-top: 24px;
|
||||
}
|
||||
</style>
|
||||
<hassio-repositories-editor hass="[[hass]]" repos="[[repos]]"></hassio-repositories-editor>
|
||||
|
||||
<template is="dom-repeat" items="[[repos]]" as="repo" sort="sortRepos">
|
||||
<hassio-addon-repository hass="[[hass]]" repo="[[repo]]" addons="[[computeAddons(repo.slug)]]"></hassio-addon-repository>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
addons: Array,
|
||||
repos: Array,
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener('hass-api-called', ev => this.apiCalled(ev));
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
apiCalled(ev) {
|
||||
if (ev.detail.success) {
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
|
||||
sortRepos(a, b) {
|
||||
if (a.slug === 'local') {
|
||||
return -1;
|
||||
} else if (b.slug === 'local') {
|
||||
return 1;
|
||||
} else if (a.slug === 'core') {
|
||||
return -1;
|
||||
} else if (b.slug === 'core') {
|
||||
return 1;
|
||||
}
|
||||
return a.name < b.name ? -1 : 1;
|
||||
}
|
||||
|
||||
computeAddons(repo) {
|
||||
return this.addons.filter(function (addon) {
|
||||
return addon.repository === repo;
|
||||
});
|
||||
}
|
||||
|
||||
loadData() {
|
||||
this.hass.callApi('get', 'hassio/addons')
|
||||
.then((info) => {
|
||||
this.addons = info.data.addons;
|
||||
this.repos = info.data.repositories;
|
||||
}, () => {
|
||||
this.addons = [];
|
||||
this.repos = [];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
refreshData() {
|
||||
this.hass.callApi('post', 'hassio/addons/reload')
|
||||
.then(() => {
|
||||
this.loadData();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-addon-store', HassioAddonStore);
|
91
hassio/src/addon-store/hassio-repositories-editor.js
Normal file
91
hassio/src/addon-store/hassio-repositories-editor.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import '@polymer/iron-icon/iron-icon.js';
|
||||
import '@polymer/paper-card/paper-card.js';
|
||||
import '@polymer/paper-input/paper-input.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../../../src/components/buttons/ha-call-api-button.js';
|
||||
import '../components/hassio-card-content.js';
|
||||
import '../resources/hassio-style.js';
|
||||
|
||||
class HassioRepositoriesEditor extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style hassio-style">
|
||||
.add {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
iron-icon {
|
||||
color: var(--secondary-text-color);
|
||||
margin-right: 16px;
|
||||
display: inline-block;
|
||||
}
|
||||
paper-input {
|
||||
width: calc(100% - 49px);
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
Repositories
|
||||
<div class="description">
|
||||
Configure which add-on repositories to fetch data from:
|
||||
</div>
|
||||
</div>
|
||||
<template id="list" is="dom-repeat" items="[[repoList]]" as="repo" sort="sortRepos">
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<hassio-card-content hass="[[hass]]" title="[[repo.name]]" description="[[repo.url]]" icon="hassio:github-circle"></hassio-card-content>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button hass="[[hass]]" path="hassio/supervisor/options" data="[[computeRemoveRepoData(repoList, repo.url)]]" class="warning">Remove</ha-call-api-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
<paper-card>
|
||||
<div class="card-content add">
|
||||
<iron-icon icon="hassio:github-circle"></iron-icon>
|
||||
<paper-input label="Add new repository by URL" value="{{repoUrl}}"></paper-input>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button hass="[[hass]]" path="hassio/supervisor/options" data="[[computeAddRepoData(repoList, repoUrl)]]">Add</ha-call-api-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
repos: {
|
||||
type: Array,
|
||||
observer: 'reposChanged',
|
||||
},
|
||||
repoList: Array,
|
||||
repoUrl: String,
|
||||
};
|
||||
}
|
||||
|
||||
reposChanged(repos) {
|
||||
this.repoList = repos.filter(repo => repo.slug !== 'core' && repo.slug !== 'local');
|
||||
this.repoUrl = '';
|
||||
}
|
||||
|
||||
sortRepos(a, b) {
|
||||
return a.name < b.name ? -1 : 1;
|
||||
}
|
||||
|
||||
computeRemoveRepoData(repoList, url) {
|
||||
const list = repoList.filter(repo => repo.url !== url).map(repo => repo.url);
|
||||
return { addons_repositories: list };
|
||||
}
|
||||
|
||||
computeAddRepoData(repoList, url) {
|
||||
const list = repoList ? repoList.map(repo => repo.url) : [];
|
||||
list.push(url);
|
||||
return { addons_repositories: list };
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-repositories-editor', HassioRepositoriesEditor);
|
115
hassio/src/addon-view/hassio-addon-audio.js
Normal file
115
hassio/src/addon-view/hassio-addon-audio.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'web-animations-js/web-animations-next-lite.min.js';
|
||||
|
||||
import '@polymer/paper-button/paper-button.js';
|
||||
import '@polymer/paper-card/paper-card.js';
|
||||
import '@polymer/paper-dropdown-menu/paper-dropdown-menu.js';
|
||||
import '@polymer/paper-item/paper-item.js';
|
||||
import '@polymer/paper-listbox/paper-listbox.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../../../src/resources/ha-style.js';
|
||||
import EventsMixin from '../../../src/mixins/events-mixin.js';
|
||||
|
||||
class HassioAddonAudio extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style">
|
||||
:host,
|
||||
paper-card,
|
||||
paper-dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
.errors {
|
||||
color: var(--google-red-500);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
paper-item {
|
||||
width: 450px;
|
||||
}
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
<paper-card heading="Audio">
|
||||
<div class="card-content">
|
||||
<template is="dom-if" if="[[error]]">
|
||||
<div class="errors">[[error]]</div>
|
||||
</template>
|
||||
|
||||
<paper-dropdown-menu label="Input">
|
||||
<paper-listbox slot="dropdown-content" attr-for-selected="device" selected="{{selectedInput}}">
|
||||
<template is="dom-repeat" items="[[inputDevices]]">
|
||||
<paper-item device\$="[[item.device]]">[[item.name]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
<paper-dropdown-menu label="Output">
|
||||
<paper-listbox slot="dropdown-content" attr-for-selected="device" selected="{{selectedOutput}}">
|
||||
<template is="dom-repeat" items="[[outputDevices]]">
|
||||
<paper-item device\$="[[item.device]]">[[item.name]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<paper-button on-click="_saveSettings">Save</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
addon: {
|
||||
type: Object,
|
||||
observer: 'addonChanged'
|
||||
},
|
||||
inputDevices: Array,
|
||||
outputDevices: Array,
|
||||
selectedInput: String,
|
||||
selectedOutput: String,
|
||||
error: String,
|
||||
};
|
||||
}
|
||||
|
||||
addonChanged(addon) {
|
||||
this.setProperties({
|
||||
selectedInput: addon.audio_input || 'null',
|
||||
selectedOutput: addon.audio_output || 'null'
|
||||
});
|
||||
if (this.outputDevices) return;
|
||||
|
||||
const noDevice = [{ device: 'null', name: '-' }];
|
||||
this.hass.callApi('get', 'hassio/hardware/audio').then((resp) => {
|
||||
const dev = resp.data.audio;
|
||||
const input = Object.keys(dev.input).map(key => ({ device: key, name: dev.input[key] }));
|
||||
const output = Object.keys(dev.output).map(key => ({ device: key, name: dev.output[key] }));
|
||||
this.setProperties({
|
||||
inputDevices: noDevice.concat(input),
|
||||
outputDevices: noDevice.concat(output)
|
||||
});
|
||||
}, () => {
|
||||
this.setProperties({
|
||||
inputDevices: noDevice,
|
||||
outputDevices: noDevice
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_saveSettings() {
|
||||
this.error = null;
|
||||
const path = `hassio/addons/${this.addon.slug}/options`;
|
||||
this.hass.callApi('post', path, {
|
||||
audio_input: this.selectedInput === 'null' ? null : this.selectedInput,
|
||||
audio_output: this.selectedOutput === 'null' ? null : this.selectedOutput
|
||||
}).then(() => {
|
||||
this.fire('hass-api-called', { success: true, path: path });
|
||||
}, (resp) => {
|
||||
this.error = resp.body.message;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-addon-audio', HassioAddonAudio);
|
98
hassio/src/addon-view/hassio-addon-config.js
Normal file
98
hassio/src/addon-view/hassio-addon-config.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
|
||||
import '@polymer/paper-button/paper-button.js';
|
||||
import '@polymer/paper-card/paper-card.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../../../src/components/buttons/ha-call-api-button.js';
|
||||
|
||||
class HassioAddonConfig extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style">
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
paper-card {
|
||||
display: block;
|
||||
}
|
||||
.card-actions {
|
||||
@apply --layout;
|
||||
@apply --layout-justified;
|
||||
}
|
||||
.errors {
|
||||
color: var(--google-red-500);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
iron-autogrow-textarea {
|
||||
width: 100%;
|
||||
font-family: monospace;
|
||||
}
|
||||
.syntaxerror {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
</style>
|
||||
<paper-card heading="Config">
|
||||
<div class="card-content">
|
||||
<template is="dom-if" if="[[error]]">
|
||||
<div class="errors">[[error]]</div>
|
||||
</template>
|
||||
<iron-autogrow-textarea id="config" value="{{config}}"></iron-autogrow-textarea>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button class="warning" hass="[[hass]]" path="hassio/addons/[[addonSlug]]/options" data="[[resetData]]">Reset to defaults</ha-call-api-button>
|
||||
<paper-button on-click="saveTapped" disabled="[[!configParsed]]">Save</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
addon: {
|
||||
type: Object,
|
||||
observer: 'addonChanged',
|
||||
},
|
||||
addonSlug: String,
|
||||
config: {
|
||||
type: String,
|
||||
observer: 'configChanged',
|
||||
},
|
||||
configParsed: Object,
|
||||
error: String,
|
||||
resetData: {
|
||||
type: Object,
|
||||
value: {
|
||||
options: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
addonChanged(addon) {
|
||||
this.config = addon ? JSON.stringify(addon.options, null, 2) : '';
|
||||
}
|
||||
|
||||
configChanged(config) {
|
||||
try {
|
||||
this.$.config.classList.remove('syntaxerror');
|
||||
this.configParsed = JSON.parse(config);
|
||||
} catch (err) {
|
||||
this.$.config.classList.add('syntaxerror');
|
||||
this.configParsed = null;
|
||||
}
|
||||
}
|
||||
|
||||
saveTapped() {
|
||||
this.error = null;
|
||||
|
||||
this.hass.callApi('post', `hassio/addons/${this.addonSlug}/options`, {
|
||||
options: this.configParsed
|
||||
}).catch((resp) => {
|
||||
this.error = resp.body.message;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-addon-config', HassioAddonConfig);
|
227
hassio/src/addon-view/hassio-addon-info.js
Normal file
227
hassio/src/addon-view/hassio-addon-info.js
Normal file
@@ -0,0 +1,227 @@
|
||||
import '@polymer/iron-icon/iron-icon.js';
|
||||
import '@polymer/paper-button/paper-button.js';
|
||||
import '@polymer/paper-card/paper-card.js';
|
||||
import '@polymer/paper-toggle-button/paper-toggle-button.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../../../src/components/buttons/ha-call-api-button.js';
|
||||
import '../../../src/components/ha-markdown.js';
|
||||
import '../../../src/resources/ha-style.js';
|
||||
import EventsMixin from '../../../src/mixins/events-mixin.js';
|
||||
|
||||
import '../components/hassio-card-content.js';
|
||||
|
||||
class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style">
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
paper-card {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.addon-header {
|
||||
@apply --paper-font-headline;
|
||||
}
|
||||
.light-color {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.addon-version {
|
||||
float: right;
|
||||
font-size: 15px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.description {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.logo img {
|
||||
max-height: 60px;
|
||||
margin: 16px 0;
|
||||
display: block;
|
||||
}
|
||||
.state div{
|
||||
width: 150px;
|
||||
display: inline-block;
|
||||
}
|
||||
paper-toggle-button {
|
||||
display: inline;
|
||||
}
|
||||
iron-icon.running {
|
||||
color: var(--paper-green-400);
|
||||
}
|
||||
iron-icon.stopped {
|
||||
color: var(--google-red-300);
|
||||
}
|
||||
ha-call-api-button {
|
||||
font-weight: 500;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.right {
|
||||
float: right;
|
||||
}
|
||||
ha-markdown img {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<template is="dom-if" if="[[computeUpdateAvailable(addon)]]">
|
||||
<paper-card heading="Update available! 🎉">
|
||||
<div class="card-content">
|
||||
<hassio-card-content hass="[[hass]]" title="[[addon.name]] [[addon.last_version]] is available" description="You are currently running version [[addon.version]]" icon="hassio:arrow-up-bold-circle" icon-class="update"></hassio-card-content>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button hass="[[hass]]" path="hassio/addons/[[addonSlug]]/update">Update</ha-call-api-button>
|
||||
<template is="dom-if" if="[[addon.changelog]]">
|
||||
<paper-button on-click="openChangelog">Changelog</paper-button>
|
||||
</template>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<div class="addon-header">[[addon.name]]
|
||||
<div class="addon-version light-color">
|
||||
<template is="dom-if" if="[[addon.version]]">
|
||||
[[addon.version]]
|
||||
<template is="dom-if" if="[[isRunning]]">
|
||||
<iron-icon title="Add-on is running" class="running" icon="hassio:circle"></iron-icon>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!isRunning]]">
|
||||
<iron-icon title="Add-on is stopped" class="stopped" icon="hassio:circle"></iron-icon>
|
||||
</template>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!addon.version]]">
|
||||
[[addon.last_version]]
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="description light-color">
|
||||
[[addon.description]].<br>
|
||||
Visit <a href="[[addon.url]]" target="_blank">[[addon.name]] page</a> for details.
|
||||
</div>
|
||||
<template is="dom-if" if="[[addon.logo]]">
|
||||
<a href="[[addon.url]]" target="_blank" class="logo">
|
||||
<img src="/api/hassio/addons/[[addonSlug]]/logo">
|
||||
</a>
|
||||
</template>
|
||||
<template is="dom-if" if="[[addon.version]]">
|
||||
<div class="state">
|
||||
<div>Start on boot</div>
|
||||
<paper-toggle-button on-change="startOnBootToggled" checked="[[computeStartOnBoot(addon.boot)]]"></paper-toggle-button>
|
||||
</div>
|
||||
<div class="state">
|
||||
<div>Auto update</div>
|
||||
<paper-toggle-button on-change="autoUpdateToggled" checked="[[addon.auto_update]]"></paper-toggle-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<template is="dom-if" if="[[addon.version]]">
|
||||
<paper-button class="warning" on-click="_unistallClicked">Uninstall</paper-button>
|
||||
<template is="dom-if" if="[[addon.build]]">
|
||||
<ha-call-api-button class="warning" hass="[[hass]]" path="hassio/addons/[[addonSlug]]/rebuild">Rebuild</ha-call-api-button>
|
||||
</template>
|
||||
<template is="dom-if" if="[[isRunning]]">
|
||||
<ha-call-api-button class="warning" hass="[[hass]]" path="hassio/addons/[[addonSlug]]/restart">Restart</ha-call-api-button>
|
||||
<ha-call-api-button class="warning" hass="[[hass]]" path="hassio/addons/[[addonSlug]]/stop">Stop</ha-call-api-button>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!isRunning]]">
|
||||
<ha-call-api-button hass="[[hass]]" path="hassio/addons/[[addonSlug]]/start">Start</ha-call-api-button>
|
||||
</template>
|
||||
<template is="dom-if" if="[[computeShowWebUI(addon.webui, isRunning)]]">
|
||||
<a href="[[pathWebui(addon.webui)]]" tabindex="-1" target="_blank" class="right"><paper-button>Open web UI</paper-button></a>
|
||||
</template>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!addon.version]]">
|
||||
<ha-call-api-button hass="[[hass]]" path="hassio/addons/[[addonSlug]]/install">Install</ha-call-api-button>
|
||||
</template>
|
||||
</div>
|
||||
</paper-card>
|
||||
<template is="dom-if" if="[[addon.long_description]]">
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<ha-markdown content="[[addon.long_description]]"></ha-markdown>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
addon: Object,
|
||||
addonSlug: String,
|
||||
isRunning: {
|
||||
type: Boolean,
|
||||
computed: 'computeIsRunning(addon)',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
computeIsRunning(addon) {
|
||||
return addon && addon.state === 'started';
|
||||
}
|
||||
|
||||
computeUpdateAvailable(addon) {
|
||||
return addon && !addon.detached && addon.version && addon.version !== addon.last_version;
|
||||
}
|
||||
|
||||
pathWebui(webui) {
|
||||
return webui && webui.replace('[HOST]', document.location.hostname);
|
||||
}
|
||||
|
||||
computeShowWebUI(webui, isRunning) {
|
||||
return webui && isRunning;
|
||||
}
|
||||
|
||||
computeStartOnBoot(state) {
|
||||
return state === 'auto';
|
||||
}
|
||||
|
||||
startOnBootToggled() {
|
||||
const data = { boot: this.addon.boot === 'auto' ? 'manual' : 'auto' };
|
||||
this.hass.callApi('POST', `hassio/addons/${this.addonSlug}/options`, data);
|
||||
}
|
||||
|
||||
autoUpdateToggled() {
|
||||
const data = { auto_update: !this.addon.auto_update };
|
||||
this.hass.callApi('POST', `hassio/addons/${this.addonSlug}/options`, data);
|
||||
}
|
||||
|
||||
openChangelog() {
|
||||
this.hass.callApi('get', `hassio/addons/${this.addonSlug}/changelog`)
|
||||
.then(
|
||||
resp => resp
|
||||
, () => 'Error getting changelog'
|
||||
).then((content) => {
|
||||
this.fire('hassio-markdown-dialog', {
|
||||
title: 'Changelog',
|
||||
content: content,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_unistallClicked() {
|
||||
if (!confirm('Are you sure you want to uninstall this add-on?')) {
|
||||
return;
|
||||
}
|
||||
const path = `hassio/addons/${this.addonSlug}/uninstall`;
|
||||
const eventData = {
|
||||
path: path,
|
||||
};
|
||||
this.hass.callApi('post', path).then((resp) => {
|
||||
eventData.success = true;
|
||||
eventData.response = resp;
|
||||
}, (resp) => {
|
||||
eventData.success = false;
|
||||
eventData.response = resp;
|
||||
}).then(() => {
|
||||
this.fire('hass-api-called', eventData);
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define('hassio-addon-info', HassioAddonInfo);
|
59
hassio/src/addon-view/hassio-addon-logs.js
Normal file
59
hassio/src/addon-view/hassio-addon-logs.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import '@polymer/paper-button/paper-button.js';
|
||||
import '@polymer/paper-card/paper-card.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../../../src/resources/ha-style.js';
|
||||
|
||||
class HassioAddonLogs extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style">
|
||||
:host,
|
||||
paper-card {
|
||||
display: block;
|
||||
}
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
<paper-card heading="Log">
|
||||
<div class="card-content">
|
||||
<pre>[[log]]</pre>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<paper-button on-click="refresh">Refresh</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
addonSlug: {
|
||||
type: String,
|
||||
observer: 'addonSlugChanged',
|
||||
},
|
||||
log: String,
|
||||
};
|
||||
}
|
||||
|
||||
addonSlugChanged(slug) {
|
||||
if (!this.hass) {
|
||||
setTimeout(() => { this.addonChanged(slug); }, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.hass.callApi('get', `hassio/addons/${this.addonSlug}/logs`)
|
||||
.then((info) => {
|
||||
this.log = info;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-addon-logs', HassioAddonLogs);
|
108
hassio/src/addon-view/hassio-addon-network.js
Normal file
108
hassio/src/addon-view/hassio-addon-network.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import '@polymer/paper-card/paper-card.js';
|
||||
import '@polymer/paper-input/paper-input.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../../../src/components/buttons/ha-call-api-button.js';
|
||||
import '../../../src/resources/ha-style.js';
|
||||
import EventsMixin from '../../../src/mixins/events-mixin.js';
|
||||
|
||||
class HassioAddonNetwork extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style">
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
paper-card {
|
||||
display: block;
|
||||
}
|
||||
.errors {
|
||||
color: var(--google-red-500);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-actions {
|
||||
@apply --layout;
|
||||
@apply --layout-justified;
|
||||
}
|
||||
</style>
|
||||
<paper-card heading="Network">
|
||||
<div class="card-content">
|
||||
<template is="dom-if" if="[[error]]">
|
||||
<div class="errors">[[error]]</div>
|
||||
</template>
|
||||
|
||||
<table>
|
||||
<tbody><tr>
|
||||
<th>Container</th>
|
||||
<th>Host</th>
|
||||
</tr>
|
||||
<template is="dom-repeat" items="[[config]]">
|
||||
<tr>
|
||||
<td>
|
||||
[[item.container]]
|
||||
</td>
|
||||
<td>
|
||||
<paper-input value="{{item.host}}" no-label-float=""></paper-input>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody></table>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button class="warning" hass="[[hass]]" path="hassio/addons/[[addonSlug]]/options" data="[[resetData]]">Reset to defaults</ha-call-api-button>
|
||||
<paper-button on-click="saveTapped">Save</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
addonSlug: String,
|
||||
config: Object,
|
||||
addon: {
|
||||
type: Object,
|
||||
observer: 'addonChanged',
|
||||
},
|
||||
error: String,
|
||||
resetData: {
|
||||
type: Object,
|
||||
value: {
|
||||
network: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
addonChanged(addon) {
|
||||
if (!addon) return;
|
||||
|
||||
const network = addon.network || {};
|
||||
const items = Object.keys(network).map(key => ({
|
||||
container: key,
|
||||
host: network[key]
|
||||
}));
|
||||
this.config = items.sort(function (el1, el2) { return el1.host - el2.host; });
|
||||
}
|
||||
|
||||
saveTapped() {
|
||||
this.error = null;
|
||||
const data = {};
|
||||
this.config.forEach(function (item) {
|
||||
data[item.container] = parseInt(item.host);
|
||||
});
|
||||
const path = `hassio/addons/${this.addonSlug}/options`;
|
||||
|
||||
this.hass.callApi('post', path, {
|
||||
network: data
|
||||
}).then(() => {
|
||||
this.fire('hass-api-called', { success: true, path: path });
|
||||
}, (resp) => {
|
||||
this.error = resp.body.message;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-addon-network', HassioAddonNetwork);
|
148
hassio/src/addon-view/hassio-addon-view.js
Normal file
148
hassio/src/addon-view/hassio-addon-view.js
Normal file
@@ -0,0 +1,148 @@
|
||||
import '@polymer/app-layout/app-header-layout/app-header-layout.js';
|
||||
import '@polymer/app-layout/app-header/app-header.js';
|
||||
import '@polymer/app-layout/app-toolbar/app-toolbar.js';
|
||||
import '@polymer/app-route/app-route.js';
|
||||
import '@polymer/paper-icon-button/paper-icon-button.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../../../src/components/ha-menu-button.js';
|
||||
import '../../../src/resources/ha-style.js';
|
||||
import '../hassio-markdown-dialog.js';
|
||||
import './hassio-addon-audio.js';
|
||||
import './hassio-addon-config.js';
|
||||
import './hassio-addon-info.js';
|
||||
import './hassio-addon-logs.js';
|
||||
import './hassio-addon-network.js';
|
||||
|
||||
class HassioAddonView extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
--paper-card-header-color: var(--primary-text-color);
|
||||
}
|
||||
.content {
|
||||
padding: 24px 0 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
hassio-addon-info,
|
||||
hassio-addon-network,
|
||||
hassio-addon-audio,
|
||||
hassio-addon-config {
|
||||
margin-bottom: 24px;
|
||||
width: 600px;
|
||||
}
|
||||
hassio-addon-logs {
|
||||
max-width: calc(100% - 8px);
|
||||
min-width: 600px;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
hassio-addon-info,
|
||||
hassio-addon-network,
|
||||
hassio-addon-audio,
|
||||
hassio-addon-config,
|
||||
hassio-addon-logs {
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<app-route route="[[route]]" pattern="/addon/:slug" data="{{routeData}}" active="{{routeMatches}}"></app-route>
|
||||
<app-header-layout has-scrolling-region="">
|
||||
<app-header fixed="" slot="header">
|
||||
<app-toolbar>
|
||||
<ha-menu-button hassio narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button>
|
||||
<paper-icon-button icon="hassio:arrow-left" on-click="backTapped"></paper-icon-button>
|
||||
<div main-title="">Hass.io: add-on details</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
<div class="content">
|
||||
<hassio-addon-info hass="[[hass]]" addon="[[addon]]" addon-slug="[[routeData.slug]]"></hassio-addon-info>
|
||||
|
||||
<template is="dom-if" if="[[addon.version]]">
|
||||
<hassio-addon-config hass="[[hass]]" addon="[[addon]]" addon-slug="[[routeData.slug]]"></hassio-addon-config>
|
||||
|
||||
<template is="dom-if" if="[[addon.audio]]">
|
||||
<hassio-addon-audio hass="[[hass]]" addon="[[addon]]"></hassio-addon-audio>
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[addon.network]]">
|
||||
<hassio-addon-network hass="[[hass]]" addon="[[addon]]" addon-slug="[[routeData.slug]]"></hassio-addon-network>
|
||||
</template>
|
||||
|
||||
<hassio-addon-logs hass="[[hass]]" addon-slug="[[routeData.slug]]"></hassio-addon-logs>
|
||||
</template>
|
||||
</div>
|
||||
</app-header-layout>
|
||||
|
||||
<hassio-markdown-dialog title="[[markdownTitle]]" content="[[markdownContent]]"></hassio-markdown-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
showMenu: Boolean,
|
||||
narrow: Boolean,
|
||||
route: Object,
|
||||
routeData: {
|
||||
type: Object,
|
||||
observer: 'routeDataChanged',
|
||||
},
|
||||
routeMatches: Boolean,
|
||||
addon: Object,
|
||||
|
||||
markdownTitle: String,
|
||||
markdownContent: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener('hass-api-called', ev => this.apiCalled(ev));
|
||||
this.addEventListener('hassio-markdown-dialog', ev => this.openMarkdown(ev));
|
||||
}
|
||||
|
||||
apiCalled(ev) {
|
||||
const path = ev.detail.path;
|
||||
|
||||
if (!path) return;
|
||||
|
||||
if (path.substr(path.lastIndexOf('/') + 1) === 'uninstall') {
|
||||
this.backTapped();
|
||||
} else {
|
||||
this.routeDataChanged(this.routeData);
|
||||
}
|
||||
}
|
||||
|
||||
routeDataChanged(routeData) {
|
||||
if (!this.routeMatches || !routeData || !routeData.slug) return;
|
||||
this.hass.callApi('get', `hassio/addons/${routeData.slug}/info`)
|
||||
.then((info) => {
|
||||
this.addon = info.data;
|
||||
}, () => {
|
||||
this.addon = null;
|
||||
});
|
||||
}
|
||||
|
||||
backTapped() {
|
||||
history.back();
|
||||
}
|
||||
|
||||
openMarkdown(ev) {
|
||||
this.setProperties({
|
||||
markdownTitle: ev.detail.title,
|
||||
markdownContent: ev.detail.content,
|
||||
});
|
||||
this.shadowRoot.querySelector('hassio-markdown-dialog').openDialog();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-addon-view', HassioAddonView);
|
75
hassio/src/components/hassio-card-content.js
Normal file
75
hassio/src/components/hassio-card-content.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import '@polymer/iron-icon/iron-icon.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../../../src/components/ha-relative-time.js';
|
||||
|
||||
class HassioCardContent extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
iron-icon {
|
||||
margin-right: 16px;
|
||||
margin-top: 16px;
|
||||
float: left;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
iron-icon.update {
|
||||
color: var(--paper-orange-400);
|
||||
}
|
||||
iron-icon.running,
|
||||
iron-icon.installed {
|
||||
color: var(--paper-green-400);
|
||||
}
|
||||
iron-icon.hassupdate,
|
||||
iron-icon.snapshot {
|
||||
color: var(--paper-item-icon-color);
|
||||
}
|
||||
.title {
|
||||
color: var(--primary-text-color);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.addition {
|
||||
color: var(--secondary-text-color);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 2.4em;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
ha-relative-time {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<iron-icon icon="[[icon]]" class\$="[[iconClass]]" title="[[iconTitle]]"></iron-icon>
|
||||
<div>
|
||||
<div class="title">[[title]]</div>
|
||||
<div class="addition">
|
||||
<template is="dom-if" if="[[description]]">
|
||||
[[description]]
|
||||
</template>
|
||||
<template is="dom-if" if="[[datetime]]">
|
||||
<ha-relative-time hass="[[hass]]" class="addition" datetime="[[datetime]]"></ha-relative-time>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
title: String,
|
||||
description: String,
|
||||
datetime: String,
|
||||
icon: {
|
||||
type: String,
|
||||
value: 'hass:help-circle'
|
||||
},
|
||||
iconTitle: String,
|
||||
iconClass: String,
|
||||
};
|
||||
}
|
||||
}
|
||||
customElements.define('hassio-card-content', HassioCardContent);
|
73
hassio/src/dashboard/hassio-addons.js
Normal file
73
hassio/src/dashboard/hassio-addons.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import '@polymer/paper-card/paper-card.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../components/hassio-card-content.js';
|
||||
import '../resources/hassio-style.js';
|
||||
import NavigateMixin from '../../../src/mixins/navigate-mixin.js';
|
||||
|
||||
class HassioAddons extends NavigateMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style hassio-style">
|
||||
paper-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<div class="content card-group">
|
||||
<div class="title">Add-ons</div>
|
||||
<template is="dom-if" if="[[!addons.length]]">
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
You don't have any add-ons installed yet. Head over to <a href="#" on-click="openStore">the add-on store</a> to get started!
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
<template is="dom-repeat" items="[[addons]]" as="addon" sort="sortAddons">
|
||||
<paper-card on-click="addonTapped">
|
||||
<div class="card-content">
|
||||
<hassio-card-content hass="[[hass]]" title="[[addon.name]]" description="[[addon.description]]" icon="[[computeIcon(addon)]]" icon-title="[[computeIconTitle(addon)]]" icon-class="[[computeIconClass(addon)]]"></hassio-card-content>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
addons: Array,
|
||||
};
|
||||
}
|
||||
|
||||
sortAddons(a, b) {
|
||||
return a.name < b.name ? -1 : 1;
|
||||
}
|
||||
|
||||
computeIcon(addon) {
|
||||
return addon.installed !== addon.version ? 'hassio:arrow-up-bold-circle' : 'hassio:puzzle';
|
||||
}
|
||||
|
||||
computeIconTitle(addon) {
|
||||
if (addon.installed !== addon.version) return 'New version available';
|
||||
return addon.state === 'started' ? 'Add-on is running' : 'Add-on is stopped';
|
||||
}
|
||||
|
||||
computeIconClass(addon) {
|
||||
if (addon.installed !== addon.version) return 'update';
|
||||
return addon.state === 'started' ? 'running' : '';
|
||||
}
|
||||
|
||||
addonTapped(ev) {
|
||||
this.navigate('/hassio/addon/' + ev.model.addon.slug);
|
||||
ev.target.blur();
|
||||
}
|
||||
|
||||
openStore(ev) {
|
||||
this.navigate('/hassio/store');
|
||||
ev.target.blur();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-addons', HassioAddons);
|
32
hassio/src/dashboard/hassio-dashboard.js
Normal file
32
hassio/src/dashboard/hassio-dashboard.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import './hassio-addons.js';
|
||||
import './hassio-hass-update.js';
|
||||
import EventsMixin from '../../../src/mixins/events-mixin.js';
|
||||
|
||||
class HassioDashboard extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
.content {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
<div class="content">
|
||||
<hassio-hass-update hass="[[hass]]" hass-info="[[hassInfo]]"></hassio-hass-update>
|
||||
<hassio-addons hass="[[hass]]" addons="[[supervisorInfo.addons]]"></hassio-addons>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
supervisorInfo: Object,
|
||||
hassInfo: Object,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-dashboard', HassioDashboard);
|
78
hassio/src/dashboard/hassio-hass-update.js
Normal file
78
hassio/src/dashboard/hassio-hass-update.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import '@polymer/paper-button/paper-button.js';
|
||||
import '@polymer/paper-card/paper-card.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../../../src/components/buttons/ha-call-api-button.js';
|
||||
import '../components/hassio-card-content.js';
|
||||
import '../resources/hassio-style.js';
|
||||
|
||||
class HassioHassUpdate extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style hassio-style">
|
||||
paper-card {
|
||||
display: block;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.errors {
|
||||
color: var(--google-red-500);
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
<template is="dom-if" if="[[computeUpdateAvailable(hassInfo)]]">
|
||||
<div class="content">
|
||||
<div class="card-group">
|
||||
<div class="title">Update available! 🎉</div>
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<hassio-card-content hass="[[hass]]" title="Home Assistant [[hassInfo.last_version]] is available" description="You are currently running version [[hassInfo.version]]" icon="hassio:home-assistant" icon-class="hassupdate"></hassio-card-content>
|
||||
<template is="dom-if" if="[[error]]">
|
||||
<div class="error">Error: [[error]]</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button hass="[[hass]]" path="hassio/homeassistant/update">Update</ha-call-api-button>
|
||||
<a href="https://github.com/home-assistant/home-assistant/releases" target="_blank"><paper-button>Release notes</paper-button></a>
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
hassInfo: Object,
|
||||
error: String,
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener('hass-api-called', ev => this.apiCalled(ev));
|
||||
}
|
||||
|
||||
apiCalled(ev) {
|
||||
if (ev.detail.success) {
|
||||
this.errors = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const response = ev.detail.response;
|
||||
|
||||
if (typeof response.body === 'object') {
|
||||
this.errors = response.body.message || 'Unknown error';
|
||||
} else {
|
||||
this.errors = response.body;
|
||||
}
|
||||
}
|
||||
|
||||
computeUpdateAvailable(hassInfo) {
|
||||
return hassInfo.version !== hassInfo.last_version;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-hass-update', HassioHassUpdate);
|
5
hassio/src/entrypoint.js
Normal file
5
hassio/src/entrypoint.js
Normal file
@@ -0,0 +1,5 @@
|
||||
window.loadES5Adapter().then(() => {
|
||||
import(/* webpackChunkName: "hassio-icons" */ './resources/hassio-icons.js');
|
||||
import(/* webpackChunkName: "hassio-main" */ './hassio-main.js');
|
||||
});
|
||||
|
46
hassio/src/hassio-app.js
Normal file
46
hassio/src/hassio-app.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import './hassio-main.js';
|
||||
import './resources/hassio-icons.js';
|
||||
|
||||
class HassioApp extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<template is="dom-if" if="[[hass]]">
|
||||
<hassio-main hass="[[hass]]" narrow="[[narrow]]" show-menu="[[showMenu]]" route="[[route]]"></hassio-main>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
narrow: Boolean,
|
||||
showMenu: Boolean,
|
||||
route: Object,
|
||||
hassioPanel: {
|
||||
type: Object,
|
||||
value: window.parent.hassioPanel,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
window.setProperties = this.setProperties.bind(this);
|
||||
this.addEventListener('location-changed', () => this._locationChanged());
|
||||
this.addEventListener('hass-open-menu', () => this._menuEvent(true));
|
||||
this.addEventListener('hass-close-menu', () => this._menuEvent(false));
|
||||
}
|
||||
|
||||
_menuEvent(shouldOpen) {
|
||||
this.hassioPanel.fire(shouldOpen ? 'hass-open-menu' : 'hass-close-menu');
|
||||
}
|
||||
|
||||
_locationChanged() {
|
||||
this.hassioPanel.navigate(window.location.pathname);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-app', HassioApp);
|
@@ -1,26 +1,22 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer-element.html">
|
||||
|
||||
<script>
|
||||
class HassioData extends Polymer.Element {
|
||||
static get is() { return 'hassio-data'; }
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
class HassioData extends PolymerElement {
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
|
||||
supervisor: {
|
||||
type: Object,
|
||||
value: {},
|
||||
notify: true,
|
||||
},
|
||||
|
||||
host: {
|
||||
type: Object,
|
||||
value: {},
|
||||
notify: true,
|
||||
},
|
||||
|
||||
homeassistant: {
|
||||
type: Object,
|
||||
value: {},
|
||||
notify: true,
|
||||
},
|
||||
};
|
||||
@@ -41,25 +37,24 @@ class HassioData extends Polymer.Element {
|
||||
|
||||
fetchSupervisorInfo() {
|
||||
return this.hass.callApi('get', 'hassio/supervisor/info')
|
||||
.then(function (info) {
|
||||
.then((info) => {
|
||||
this.supervisor = info.data;
|
||||
}.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
fetchHostInfo() {
|
||||
return this.hass.callApi('get', 'hassio/host/info')
|
||||
.then(function (info) {
|
||||
.then((info) => {
|
||||
this.host = info.data;
|
||||
}.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
fetchHassInfo() {
|
||||
return this.hass.callApi('get', 'hassio/homeassistant/info')
|
||||
.then(function (info) {
|
||||
.then((info) => {
|
||||
this.homeassistant = info.data;
|
||||
}.bind(this));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(HassioData.is, HassioData);
|
||||
</script>
|
||||
customElements.define('hassio-data', HassioData);
|
103
hassio/src/hassio-main.js
Normal file
103
hassio/src/hassio-main.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import '@polymer/app-route/app-route.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../../src/layouts/hass-loading-screen.js';
|
||||
import './addon-view/hassio-addon-view.js';
|
||||
import './hassio-data.js';
|
||||
import './hassio-pages-with-tabs.js';
|
||||
|
||||
import applyThemesOnElement from '../../src/common/dom/apply_themes_on_element.js';
|
||||
import NavigateMixin from '../../src/mixins/navigate-mixin.js';
|
||||
|
||||
class HassioMain extends NavigateMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<app-route route="[[route]]" pattern="/:page" data="{{routeData}}"></app-route>
|
||||
<hassio-data id="data" hass="[[hass]]" supervisor="{{supervisorInfo}}" homeassistant="{{hassInfo}}" host="{{hostInfo}}"></hassio-data>
|
||||
|
||||
<template is="dom-if" if="[[!loaded]]">
|
||||
<hass-loading-screen narrow="[[narrow]]" show-menu="[[showMenu]]"></hass-loading-screen>
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[loaded]]">
|
||||
<template is="dom-if" if="[[!equalsAddon(routeData.page)]]">
|
||||
<hassio-pages-with-tabs hass="[[hass]]" narrow="[[narrow]]" show-menu="[[showMenu]]" page="[[routeData.page]]" supervisor-info="[[supervisorInfo]]" hass-info="[[hassInfo]]" host-info="[[hostInfo]]"></hassio-pages-with-tabs>
|
||||
</template>
|
||||
<template is="dom-if" if="[[equalsAddon(routeData.page)]]">
|
||||
<hassio-addon-view hass="[[hass]]" narrow="[[narrow]]" show-menu="[[showMenu]]" route="[[route]]"></hassio-addon-view>
|
||||
</template>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
narrow: Boolean,
|
||||
showMenu: Boolean,
|
||||
route: {
|
||||
type: Object,
|
||||
// Fake route object
|
||||
value: {
|
||||
prefix: '/hassio',
|
||||
path: '/dashboard',
|
||||
__queryParams: {}
|
||||
},
|
||||
observer: 'routeChanged',
|
||||
},
|
||||
routeData: Object,
|
||||
supervisorInfo: Object,
|
||||
hostInfo: Object,
|
||||
hassInfo: Object,
|
||||
loaded: {
|
||||
type: Boolean,
|
||||
computed: 'computeIsLoaded(supervisorInfo, hostInfo, hassInfo)',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
applyThemesOnElement(this, this.hass.themes, this.hass.selectedTheme, true);
|
||||
this.addEventListener('hass-api-called', ev => this.apiCalled(ev));
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.routeChanged(this.route);
|
||||
}
|
||||
|
||||
apiCalled(ev) {
|
||||
if (ev.detail.success) {
|
||||
let tries = 1;
|
||||
|
||||
const tryUpdate = () => {
|
||||
this.$.data.refresh().catch(function () {
|
||||
tries += 1;
|
||||
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
|
||||
});
|
||||
};
|
||||
|
||||
tryUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
computeIsLoaded(supervisorInfo, hostInfo, hassInfo) {
|
||||
return (supervisorInfo !== null &&
|
||||
hostInfo !== null &&
|
||||
hassInfo !== null);
|
||||
}
|
||||
|
||||
routeChanged(route) {
|
||||
if (route.path === '' && route.prefix === '/hassio') {
|
||||
this.navigate('/hassio/dashboard', true);
|
||||
}
|
||||
}
|
||||
|
||||
equalsAddon(page) {
|
||||
return page && page === 'addon';
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-main', HassioMain);
|
76
hassio/src/hassio-markdown-dialog.js
Normal file
76
hassio/src/hassio-markdown-dialog.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import '@polymer/app-layout/app-toolbar/app-toolbar.js';
|
||||
import '@polymer/paper-dialog-scrollable/paper-dialog-scrollable.js';
|
||||
import '@polymer/paper-dialog/paper-dialog.js';
|
||||
import '@polymer/paper-icon-button/paper-icon-button.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../../src/components/ha-markdown.js';
|
||||
import '../../src/resources/ha-style.js';
|
||||
|
||||
class HassioMarkdownDialog extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style-dialog">
|
||||
paper-dialog {
|
||||
min-width: 350px;
|
||||
font-size: 14px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
app-toolbar {
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(--secondary-background-color);
|
||||
}
|
||||
app-toolbar [main-title] {
|
||||
margin-left: 16px;
|
||||
}
|
||||
paper-checkbox {
|
||||
display: block;
|
||||
margin: 4px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
paper-dialog {
|
||||
max-height: 100%;
|
||||
}
|
||||
paper-dialog::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
background-color: inherit;
|
||||
}
|
||||
app-toolbar {
|
||||
color: var(--text-primary-color);
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<paper-dialog id="dialog" with-backdrop="">
|
||||
<app-toolbar>
|
||||
<paper-icon-button icon="hassio:close" dialog-dismiss=""></paper-icon-button>
|
||||
<div main-title="">[[title]]</div>
|
||||
</app-toolbar>
|
||||
<paper-dialog-scrollable>
|
||||
<ha-markdown content="[[content]]"></ha-markdown>
|
||||
</paper-dialog-scrollable>
|
||||
</paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
title: String,
|
||||
content: String,
|
||||
};
|
||||
}
|
||||
|
||||
openDialog() {
|
||||
this.$.dialog.open();
|
||||
}
|
||||
}
|
||||
customElements.define('hassio-markdown-dialog', HassioMarkdownDialog);
|
133
hassio/src/hassio-pages-with-tabs.js
Normal file
133
hassio/src/hassio-pages-with-tabs.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import '@polymer/app-layout/app-header-layout/app-header-layout.js';
|
||||
import '@polymer/app-layout/app-header/app-header.js';
|
||||
import '@polymer/app-layout/app-toolbar/app-toolbar.js';
|
||||
import '@polymer/paper-icon-button/paper-icon-button.js';
|
||||
import '@polymer/paper-tabs/paper-tab.js';
|
||||
import '@polymer/paper-tabs/paper-tabs.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../../src/components/ha-menu-button.js';
|
||||
import '../../src/resources/ha-style.js';
|
||||
import './addon-store/hassio-addon-store.js';
|
||||
import './dashboard/hassio-dashboard.js';
|
||||
import './hassio-markdown-dialog.js';
|
||||
import './snapshots/hassio-snapshot.js';
|
||||
import './snapshots/hassio-snapshots.js';
|
||||
import './system/hassio-system.js';
|
||||
|
||||
import scrollToTarget from '../../src/common/dom/scroll-to-target.js';
|
||||
|
||||
import NavigateMixin from '../../src/mixins/navigate-mixin.js';
|
||||
|
||||
class HassioPagesWithTabs extends NavigateMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex iron-positioning ha-style">
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
--paper-card-header-color: var(--primary-text-color);
|
||||
}
|
||||
paper-tabs {
|
||||
margin-left: 12px;
|
||||
--paper-tabs-selection-bar-color: #FFF;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
<app-header-layout id="layout" has-scrolling-region>
|
||||
<app-header fixed slot="header">
|
||||
<app-toolbar>
|
||||
<ha-menu-button hassio narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button>
|
||||
<div main-title>Hass.io</div>
|
||||
<template is="dom-if" if="[[showRefreshButton(page)]]">
|
||||
<paper-icon-button icon="hassio:refresh" on-click="refreshClicked"></paper-icon-button>
|
||||
</template>
|
||||
</app-toolbar>
|
||||
<paper-tabs scrollable="" selected="[[page]]" attr-for-selected="page-name" on-iron-activate="handlePageSelected">
|
||||
<paper-tab page-name="dashboard">Dashboard</paper-tab>
|
||||
<paper-tab page-name="snapshots">Snapshots</paper-tab>
|
||||
<paper-tab page-name="store">Add-on store</paper-tab>
|
||||
<paper-tab page-name="system">System</paper-tab>
|
||||
</paper-tabs>
|
||||
</app-header>
|
||||
<template is="dom-if" if="[[equals(page, "dashboard")]]">
|
||||
<hassio-dashboard hass="[[hass]]" supervisor-info="[[supervisorInfo]]" hass-info="[[hassInfo]]"></hassio-dashboard>
|
||||
</template>
|
||||
<template is="dom-if" if="[[equals(page, "snapshots")]]">
|
||||
<hassio-snapshots hass="[[hass]]" installed-addons="[[supervisorInfo.addons]]" snapshot-slug="{{snapshotSlug}}" snapshot-deleted="{{snapshotDeleted}}"></hassio-snapshots>
|
||||
</template>
|
||||
<template is="dom-if" if="[[equals(page, "store")]]">
|
||||
<hassio-addon-store hass="[[hass]]"></hassio-addon-store>
|
||||
</template>
|
||||
<template is="dom-if" if="[[equals(page, "system")]]">
|
||||
<hassio-system hass="[[hass]]" supervisor-info="[[supervisorInfo]]" host-info="[[hostInfo]]"></hassio-system>
|
||||
</template>
|
||||
</app-header-layout>
|
||||
|
||||
<hassio-markdown-dialog title="[[markdownTitle]]" content="[[markdownContent]]"></hassio-markdown-dialog>
|
||||
|
||||
<template is="dom-if" if="[[equals(page, "snapshots")]]">
|
||||
<hassio-snapshot hass="[[hass]]" snapshot-slug="{{snapshotSlug}}" snapshot-deleted="{{snapshotDeleted}}"></hassio-snapshot>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
showMenu: Boolean,
|
||||
narrow: Boolean,
|
||||
page: String,
|
||||
supervisorInfo: Object,
|
||||
hostInfo: Object,
|
||||
hassInfo: Object,
|
||||
snapshotSlug: String,
|
||||
snapshotDeleted: Boolean,
|
||||
|
||||
markdownTitle: String,
|
||||
markdownContent: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener('hassio-markdown-dialog', ev => this.openMarkdown(ev));
|
||||
}
|
||||
|
||||
handlePageSelected(ev) {
|
||||
const newPage = ev.detail.item.getAttribute('page-name');
|
||||
if (newPage !== this.page) {
|
||||
this.navigate(`/hassio/${newPage}`);
|
||||
}
|
||||
scrollToTarget(this, this.$.layout.header.scrollTarget);
|
||||
}
|
||||
|
||||
equals(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
showRefreshButton(page) {
|
||||
return page === 'store' || page === 'snapshots';
|
||||
}
|
||||
|
||||
refreshClicked() {
|
||||
if (this.page === 'snapshots') {
|
||||
this.shadowRoot.querySelector('hassio-snapshots').refreshData();
|
||||
} else {
|
||||
this.shadowRoot.querySelector('hassio-addon-store').refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
openMarkdown(ev) {
|
||||
this.setProperties({
|
||||
markdownTitle: ev.detail.title,
|
||||
markdownContent: ev.detail.content,
|
||||
});
|
||||
this.shadowRoot.querySelector('hassio-markdown-dialog').openDialog();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-pages-with-tabs', HassioPagesWithTabs);
|
7
hassio/src/resources/hassio-icons.js
Normal file
7
hassio/src/resources/hassio-icons.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import '../../../src/components/ha-iconset-svg.js';
|
||||
import iconSetContent from '../../hassio-icons.html';
|
||||
|
||||
const documentContainer = document.createElement('template');
|
||||
documentContainer.setAttribute('style', 'display: none;');
|
||||
documentContainer.innerHTML = iconSetContent;
|
||||
document.head.appendChild(documentContainer.content);
|
58
hassio/src/resources/hassio-style.js
Normal file
58
hassio/src/resources/hassio-style.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const documentContainer = document.createElement('template');
|
||||
documentContainer.setAttribute('style', 'display: none;');
|
||||
|
||||
documentContainer.innerHTML = `<dom-module id="hassio-style">
|
||||
<template>
|
||||
<style>
|
||||
.card-group {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.card-group .title {
|
||||
color: var(--primary-text-color);
|
||||
font-size: 2em;
|
||||
padding-left: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.card-group .description {
|
||||
font-size: 0.5em;
|
||||
font-weight: 500;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.card-group paper-card {
|
||||
--card-group-columns: 4;
|
||||
width: calc((100% - 12px * var(--card-group-columns)) / var(--card-group-columns));
|
||||
margin: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
@media screen and (max-width: 1200px) and (min-width: 901px) {
|
||||
.card-group paper-card {
|
||||
--card-group-columns: 3;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 900px) and (min-width: 601px) {
|
||||
.card-group paper-card {
|
||||
--card-group-columns: 2;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 600px) and (min-width: 0) {
|
||||
.card-group paper-card {
|
||||
width: 100%;
|
||||
margin: 4px 0;
|
||||
}
|
||||
.content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
ha-call-api-button {
|
||||
font-weight: 500;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
</dom-module>`;
|
||||
|
||||
document.head.appendChild(documentContainer.content);
|
255
hassio/src/snapshots/hassio-snapshot.js
Normal file
255
hassio/src/snapshots/hassio-snapshot.js
Normal file
@@ -0,0 +1,255 @@
|
||||
import '@polymer/app-layout/app-toolbar/app-toolbar.js';
|
||||
import '@polymer/paper-button/paper-button.js';
|
||||
import '@polymer/paper-checkbox/paper-checkbox.js';
|
||||
import '@polymer/paper-dialog-scrollable/paper-dialog-scrollable.js';
|
||||
import '@polymer/paper-dialog/paper-dialog.js';
|
||||
import '@polymer/paper-icon-button/paper-icon-button.js';
|
||||
import '@polymer/paper-input/paper-input.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../../../src/resources/ha-style.js';
|
||||
|
||||
class HassioSnapshot extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style-dialog">
|
||||
paper-dialog {
|
||||
min-width: 350px;
|
||||
font-size: 14px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
app-toolbar {
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(--secondary-background-color);
|
||||
}
|
||||
app-toolbar [main-title] {
|
||||
margin-left: 16px;
|
||||
}
|
||||
paper-dialog-scrollable {
|
||||
margin: 0;
|
||||
}
|
||||
paper-checkbox {
|
||||
display: block;
|
||||
margin: 4px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
paper-dialog {
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
app-toolbar {
|
||||
color: var(--text-primary-color);
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
.details {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.download {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.warning,
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
</style>
|
||||
<paper-dialog id="dialog" with-backdrop="" on-iron-overlay-closed="_dialogClosed">
|
||||
<app-toolbar>
|
||||
<paper-icon-button icon="hassio:close" dialog-dismiss=""></paper-icon-button>
|
||||
<div main-title="">[[_computeName(snapshot)]]</div>
|
||||
</app-toolbar>
|
||||
<div class="details">
|
||||
[[_computeType(snapshot.type)]] ([[_computeSize(snapshot.size)]])<br>
|
||||
[[_formatDatetime(snapshot.date)]]
|
||||
</div>
|
||||
<div>Home Assistant:</div>
|
||||
<paper-checkbox checked="{{restoreHass}}">
|
||||
Home Assistant [[snapshot.homeassistant]]
|
||||
</paper-checkbox>
|
||||
<template is="dom-if" if="[[snapshot.addons.length]]">
|
||||
<div>Folders:</div>
|
||||
<template is="dom-repeat" items="[[snapshot.folders]]">
|
||||
<paper-checkbox checked="{{item.checked}}">
|
||||
[[item.name]]
|
||||
</paper-checkbox>
|
||||
</template>
|
||||
</template>
|
||||
<template is="dom-if" if="[[snapshot.addons.length]]">
|
||||
<div>Add-ons:</div>
|
||||
<paper-dialog-scrollable>
|
||||
<template is="dom-repeat" items="[[snapshot.addons]]" sort="_sortAddons">
|
||||
<paper-checkbox checked="{{item.checked}}">
|
||||
[[item.name]]
|
||||
<span class="details">([[item.version]])</span>
|
||||
</paper-checkbox>
|
||||
</template>
|
||||
</paper-dialog-scrollable>
|
||||
</template>
|
||||
<template is="dom-if" if="[[snapshot.protected]]">
|
||||
<paper-input autofocus="" label="Password" type="password" value="{{snapshotPassword}}"></paper-input>
|
||||
</template>
|
||||
<template is="dom-if" if="[[error]]">
|
||||
<p class="error">Error: [[error]]</p>
|
||||
</template>
|
||||
<div class="buttons">
|
||||
<paper-icon-button icon="hassio:delete" on-click="_deleteClicked" class="warning" title="Delete snapshot"></paper-icon-button>
|
||||
<a href="[[_computeDownloadUrl(snapshotSlug)]]" download="[[_computeDownloadName(snapshot)]]">
|
||||
<paper-icon-button icon="hassio:download" class="download" title="Download snapshot"></paper-icon-button>
|
||||
</a>
|
||||
<paper-button on-click="_partialRestoreClicked">Restore selected</paper-button>
|
||||
<template is="dom-if" if="[[_isFullSnapshot(snapshot.type)]]">
|
||||
<paper-button on-click="_fullRestoreClicked">Wipe & restore</paper-button>
|
||||
</template>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
snapshotSlug: {
|
||||
type: String,
|
||||
notify: true,
|
||||
observer: '_snapshotSlugChanged',
|
||||
},
|
||||
snapshotDeleted: {
|
||||
type: Boolean,
|
||||
notify: true,
|
||||
},
|
||||
snapshot: Object,
|
||||
restoreHass: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
snapshotPassword: String,
|
||||
error: String,
|
||||
};
|
||||
}
|
||||
|
||||
_snapshotSlugChanged(snapshotSlug) {
|
||||
if (!snapshotSlug || snapshotSlug === 'update') return;
|
||||
this.hass.callApi('get', `hassio/snapshots/${snapshotSlug}/info`)
|
||||
.then((info) => {
|
||||
info.data.folders = this._computeFolders(info.data.folders);
|
||||
info.data.addons = this._computeAddons(info.data.addons);
|
||||
this.snapshot = info.data;
|
||||
this.$.dialog.open();
|
||||
}, () => {
|
||||
this.snapshot = null;
|
||||
});
|
||||
}
|
||||
|
||||
_computeFolders(folders) {
|
||||
const list = [];
|
||||
if (folders.includes('homeassistant')) list.push({ slug: 'homeassistant', name: 'Home Assistant configuration', checked: true });
|
||||
if (folders.includes('ssl')) list.push({ slug: 'ssl', name: 'SSL', checked: true });
|
||||
if (folders.includes('share')) list.push({ slug: 'share', name: 'Share', checked: true });
|
||||
if (folders.includes('addons/local')) list.push({ slug: 'addons/local', name: 'Local add-ons', checked: true });
|
||||
return list;
|
||||
}
|
||||
|
||||
_computeAddons(addons) {
|
||||
return addons.map(addon => (
|
||||
{ slug: addon.slug, name: addon.name, version: addon.version, checked: true }));
|
||||
}
|
||||
|
||||
_isFullSnapshot(type) {
|
||||
return type === 'full';
|
||||
}
|
||||
|
||||
_partialRestoreClicked() {
|
||||
if (!confirm('Are you sure you want to restore this snapshot?')) {
|
||||
return;
|
||||
}
|
||||
const addons = this.snapshot.addons.filter(addon => addon.checked).map(addon => addon.slug);
|
||||
const folders =
|
||||
this.snapshot.folders.filter(folder => folder.checked).map(folder => folder.slug);
|
||||
|
||||
const data = {
|
||||
homeassistant: this.restoreHass,
|
||||
addons: addons,
|
||||
folders: folders
|
||||
};
|
||||
if (this.snapshot.protected) data.password = this.snapshotPassword;
|
||||
|
||||
this.hass.callApi('post', `hassio/snapshots/${this.snapshotSlug}/restore/partial`, data).then(() => {
|
||||
alert('Snapshot restored!');
|
||||
this.$.dialog.close();
|
||||
}, (error) => {
|
||||
this.error = error.body.message;
|
||||
});
|
||||
}
|
||||
|
||||
_fullRestoreClicked() {
|
||||
if (!confirm('Are you sure you want to restore this snapshot?')) {
|
||||
return;
|
||||
}
|
||||
const data = this.snapshot.protected ? { password: this.snapshotPassword } : null;
|
||||
this.hass.callApi('post', `hassio/snapshots/${this.snapshotSlug}/restore/full`, data)
|
||||
.then(() => {
|
||||
alert('Snapshot restored!');
|
||||
this.$.dialog.close();
|
||||
}, (error) => {
|
||||
this.error = error.body.message;
|
||||
});
|
||||
}
|
||||
|
||||
_deleteClicked() {
|
||||
if (!confirm('Are you sure you want to delete this snapshot?')) {
|
||||
return;
|
||||
}
|
||||
this.hass.callApi('post', `hassio/snapshots/${this.snapshotSlug}/remove`)
|
||||
.then(() => {
|
||||
this.$.dialog.close();
|
||||
this.snapshotDeleted = true;
|
||||
}, (error) => {
|
||||
this.error = error.body.message;
|
||||
});
|
||||
}
|
||||
|
||||
_computeDownloadUrl(snapshotSlug) {
|
||||
const password = encodeURIComponent(this.hass.connection.options.authToken);
|
||||
return `/api/hassio/snapshots/${snapshotSlug}/download?api_password=${password}`;
|
||||
}
|
||||
|
||||
_computeDownloadName(snapshot) {
|
||||
const name = this._computeName(snapshot).replace(/[^a-z0-9]+/gi, '_');
|
||||
return `Hass_io_${name}.tar`;
|
||||
}
|
||||
|
||||
_computeName(snapshot) {
|
||||
return snapshot.name || snapshot.slug;
|
||||
}
|
||||
|
||||
_computeType(type) {
|
||||
return type === 'full' ? 'Full snapshot' : 'Partial snapshot';
|
||||
}
|
||||
|
||||
_computeSize(size) {
|
||||
return (Math.ceil(size * 10) / 10) + ' MB';
|
||||
}
|
||||
|
||||
_sortAddons(a, b) {
|
||||
return a.name < b.name ? -1 : 1;
|
||||
}
|
||||
|
||||
_formatDatetime(datetime) {
|
||||
return new Date(datetime).toLocaleDateString(navigator.language, {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
_dialogClosed() {
|
||||
this.snapshotSlug = null;
|
||||
}
|
||||
}
|
||||
customElements.define('hassio-snapshot', HassioSnapshot);
|
262
hassio/src/snapshots/hassio-snapshots.js
Normal file
262
hassio/src/snapshots/hassio-snapshots.js
Normal file
@@ -0,0 +1,262 @@
|
||||
import '@polymer/paper-button/paper-button.js';
|
||||
import '@polymer/paper-card/paper-card.js';
|
||||
import '@polymer/paper-checkbox/paper-checkbox.js';
|
||||
import '@polymer/paper-input/paper-input.js';
|
||||
import '@polymer/paper-radio-button/paper-radio-button.js';
|
||||
import '@polymer/paper-radio-group/paper-radio-group.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../components/hassio-card-content.js';
|
||||
import '../resources/hassio-style.js';
|
||||
import EventsMixin from '../../../src/mixins/events-mixin.js';
|
||||
|
||||
class HassioSnapshots extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style hassio-style">
|
||||
paper-radio-group {
|
||||
display: block;
|
||||
}
|
||||
paper-radio-button {
|
||||
padding: 0 0 2px 2px;
|
||||
}
|
||||
paper-radio-button,
|
||||
paper-checkbox,
|
||||
paper-input[type="password"] {
|
||||
display: block;
|
||||
margin: 4px 0 4px 48px;
|
||||
}
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<div class="content">
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
Create snapshot
|
||||
<div class="description">
|
||||
Snapshots allow you to easily backup and
|
||||
restore all data of your Hass.io instance.
|
||||
</div>
|
||||
</div>
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<paper-input autofocus="" label="Name" value="{{snapshotName}}"></paper-input>
|
||||
Type:
|
||||
<paper-radio-group selected="{{snapshotType}}">
|
||||
<paper-radio-button name="full">
|
||||
Full snapshot
|
||||
</paper-radio-button>
|
||||
<paper-radio-button name="partial">
|
||||
Partial snapshot
|
||||
</paper-radio-button>
|
||||
</paper-radio-group>
|
||||
<template is="dom-if" if="[[!_fullSelected(snapshotType)]]">
|
||||
Folders:
|
||||
<template is="dom-repeat" items="[[folderList]]">
|
||||
<paper-checkbox checked="{{item.checked}}">
|
||||
[[item.name]]
|
||||
</paper-checkbox>
|
||||
</template>
|
||||
Add-ons:
|
||||
<template is="dom-repeat" items="[[addonList]]" sort="_sortAddons">
|
||||
<paper-checkbox checked="{{item.checked}}">
|
||||
[[item.name]]
|
||||
</paper-checkbox>
|
||||
</template>
|
||||
</template>
|
||||
Security:
|
||||
<paper-checkbox checked="{{snapshotHasPassword}}">Password protection</paper-checkbox>
|
||||
<template is="dom-if" if="[[snapshotHasPassword]]">
|
||||
<paper-input label="Password" type="password" value="{{snapshotPassword}}"></paper-input>
|
||||
</template>
|
||||
<template is="dom-if" if="[[error]]">
|
||||
<p class="error">[[error]]</p>
|
||||
</template>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<paper-button disabled="[[creatingSnapshot]]" on-click="_createSnapshot">Create</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
|
||||
<div class="card-group">
|
||||
<div class="title">Available snapshots</div>
|
||||
<template is="dom-if" if="[[!snapshots.length]]">
|
||||
<paper-card>
|
||||
<div class="card-content">You don't have any snapshots yet.</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
<template is="dom-repeat" items="[[snapshots]]" as="snapshot" sort="_sortSnapshots">
|
||||
<paper-card class="pointer" on-click="_snapshotClicked">
|
||||
<div class="card-content">
|
||||
<hassio-card-content hass="[[hass]]" title="[[_computeName(snapshot)]]" description="[[_computeDetails(snapshot)]]" datetime="[[snapshot.date]]" icon="[[_computeIcon(snapshot.type)]]" icon-class="snapshot"></hassio-card-content>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
snapshotName: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
snapshotPassword: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
snapshotHasPassword: Boolean,
|
||||
snapshotType: {
|
||||
type: String,
|
||||
value: 'full',
|
||||
},
|
||||
snapshots: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
installedAddons: {
|
||||
type: Array,
|
||||
observer: '_installedAddonsChanged',
|
||||
},
|
||||
addonList: Array,
|
||||
folderList: {
|
||||
type: Array,
|
||||
value: [
|
||||
{ slug: 'homeassistant', name: 'Home Assistant configuration', checked: true },
|
||||
{ slug: 'ssl', name: 'SSL', checked: true },
|
||||
{ slug: 'share', name: 'Share', checked: true },
|
||||
{ slug: 'addons/local', name: 'Local add-ons', checked: true },
|
||||
],
|
||||
},
|
||||
snapshotSlug: {
|
||||
type: String,
|
||||
notify: true,
|
||||
},
|
||||
snapshotDeleted: {
|
||||
type: Boolean,
|
||||
notify: true,
|
||||
observer: '_snapshotDeletedChanged',
|
||||
},
|
||||
creatingSnapshot: Boolean,
|
||||
dialogOpened: Boolean,
|
||||
error: String,
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener('hass-api-called', ev => this._apiCalled(ev));
|
||||
this._updateSnapshots();
|
||||
}
|
||||
|
||||
_apiCalled(ev) {
|
||||
if (ev.detail.success) {
|
||||
this._updateSnapshots();
|
||||
}
|
||||
}
|
||||
|
||||
_updateSnapshots() {
|
||||
this.hass.callApi('get', 'hassio/snapshots')
|
||||
.then((result) => {
|
||||
this.snapshots = result.data.snapshots;
|
||||
}, (error) => {
|
||||
this.error = error.message;
|
||||
});
|
||||
}
|
||||
|
||||
_createSnapshot() {
|
||||
this.error = '';
|
||||
if (this.snapshotHasPassword && !this.snapshotPassword.length) {
|
||||
this.error = 'Please enter a password.';
|
||||
return;
|
||||
}
|
||||
this.creatingSnapshot = true;
|
||||
let name = this.snapshotName;
|
||||
if (!name.length) {
|
||||
name = new Date().toLocaleDateString(navigator.language, {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric' });
|
||||
}
|
||||
let data;
|
||||
let path;
|
||||
if (this.snapshotType === 'full') {
|
||||
data = { name: name };
|
||||
path = 'hassio/snapshots/new/full';
|
||||
} else {
|
||||
const addons = this.addonList.filter(addon => addon.checked).map(addon => addon.slug);
|
||||
const folders = this.folderList.filter(folder => folder.checked).map(folder => folder.slug);
|
||||
|
||||
data = { name: name, folders: folders, addons: addons };
|
||||
path = 'hassio/snapshots/new/partial';
|
||||
}
|
||||
if (this.snapshotHasPassword) {
|
||||
data.password = this.snapshotPassword;
|
||||
}
|
||||
|
||||
this.hass.callApi('post', path, data)
|
||||
.then(() => {
|
||||
this.creatingSnapshot = false;
|
||||
this.fire('hass-api-called', { success: true });
|
||||
}, (error) => {
|
||||
this.creatingSnapshot = false;
|
||||
this.error = error.message;
|
||||
});
|
||||
}
|
||||
|
||||
_installedAddonsChanged(addons) {
|
||||
this.addonList = addons.map(addon => ({ slug: addon.slug, name: addon.name, checked: true }));
|
||||
}
|
||||
|
||||
_sortAddons(a, b) {
|
||||
return a.name < b.name ? -1 : 1;
|
||||
}
|
||||
|
||||
_sortSnapshots(a, b) {
|
||||
return a.date < b.date ? 1 : -1;
|
||||
}
|
||||
|
||||
_computeName(snapshot) {
|
||||
return snapshot.name || snapshot.slug;
|
||||
}
|
||||
|
||||
_computeDetails(snapshot) {
|
||||
const type = snapshot.type === 'full' ? 'Full snapshot' : 'Partial snapshot';
|
||||
return snapshot.protected ? `${type}, password protected` : type;
|
||||
}
|
||||
|
||||
_computeIcon(type) {
|
||||
return type === 'full' ? 'hassio:package-variant-closed' : 'hassio:package-variant';
|
||||
}
|
||||
|
||||
_snapshotClicked(ev) {
|
||||
this.snapshotSlug = ev.model.snapshot.slug;
|
||||
}
|
||||
|
||||
_fullSelected(type) {
|
||||
return type === 'full';
|
||||
}
|
||||
|
||||
_snapshotDeletedChanged(snapshotDeleted) {
|
||||
if (snapshotDeleted) {
|
||||
this._updateSnapshots();
|
||||
this.snapshotDeleted = false;
|
||||
}
|
||||
}
|
||||
|
||||
refreshData() {
|
||||
this.hass.callApi('post', 'hassio/snapshots/reload')
|
||||
.then(() => {
|
||||
this._updateSnapshots();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-snapshots', HassioSnapshots);
|
186
hassio/src/system/hassio-host-info.js
Normal file
186
hassio/src/system/hassio-host-info.js
Normal file
@@ -0,0 +1,186 @@
|
||||
import '@polymer/paper-button/paper-button.js';
|
||||
import '@polymer/paper-card/paper-card.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../../../src/components/buttons/ha-call-api-button.js';
|
||||
import EventsMixin from '../../../src/mixins/events-mixin.js';
|
||||
|
||||
class HassioHostInfo extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
paper-card {
|
||||
display: inline-block;
|
||||
width: 400px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.card-content {
|
||||
height: 200px;
|
||||
}
|
||||
@media screen and (max-width: 830px) {
|
||||
paper-card {
|
||||
margin-top: 8px;
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.card-content {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.info {
|
||||
width: 100%;
|
||||
}
|
||||
.info td:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
.errors {
|
||||
color: var(--google-red-500);
|
||||
margin-top: 16px;
|
||||
}
|
||||
paper-button.info {
|
||||
max-width: calc(50% - 12px);
|
||||
}
|
||||
</style>
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<h2>Host system</h2>
|
||||
<table class="info">
|
||||
<tbody><tr>
|
||||
<td>Hostname</td>
|
||||
<td>[[data.hostname]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>System</td>
|
||||
<td>[[data.operating_system]]</td>
|
||||
</tr>
|
||||
<template is="dom-if" if="[[data.deployment]]">
|
||||
<tr>
|
||||
<td>Deployment</td>
|
||||
<td>[[data.deployment]]</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody></table>
|
||||
<paper-button raised on-click="_showHardware" class="info">
|
||||
Hardware
|
||||
</paper-button>
|
||||
<template is="dom-if" if="[[_featureAvailable(data, 'hostname')]]">
|
||||
<paper-button raised on-click="_changeHostnameClicked" class="info">
|
||||
Change hostname
|
||||
</paper-button>
|
||||
</template>
|
||||
<template is="dom-if" if="[[errors]]">
|
||||
<div class="errors">Error: [[errors]]</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<template is="dom-if" if="[[_featureAvailable(data, 'reboot')]]">
|
||||
<ha-call-api-button class="warning" hass="[[hass]]" path="hassio/host/reboot">Reboot</ha-call-api-button>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_featureAvailable(data, 'shutdown')]]">
|
||||
<ha-call-api-button class="warning" hass="[[hass]]" path="hassio/host/shutdown">Shutdown</ha-call-api-button>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_featureAvailable(data, 'hassos')]]">
|
||||
<ha-call-api-button class="warning" hass="[[hass]]" path="hassio/hassos/config/sync" title="Load HassOS configs or updates from USB">Import from USB</ha-call-api-button>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_computeUpdateAvailable(_hassOs)]]">
|
||||
<ha-call-api-button hass="[[hass]]" path="hassio/hassos/update">Update</ha-call-api-button>
|
||||
</template>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
data: {
|
||||
type: Object,
|
||||
observer: '_dataChanged'
|
||||
},
|
||||
errors: String,
|
||||
_hassOs: Object
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener('hass-api-called', ev => this.apiCalled(ev));
|
||||
}
|
||||
|
||||
apiCalled(ev) {
|
||||
if (ev.detail.success) {
|
||||
this.errors = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var response = ev.detail.response;
|
||||
|
||||
if (typeof response.body === 'object') {
|
||||
this.errors = response.body.message || 'Unknown error';
|
||||
} else {
|
||||
this.errors = response.body;
|
||||
}
|
||||
}
|
||||
|
||||
_dataChanged(data) {
|
||||
if (data.features && data.features.includes('hassos')) {
|
||||
this.hass.callApi('get', 'hassio/hassos/info')
|
||||
.then((resp) => {
|
||||
this._hassOs = resp.data;
|
||||
});
|
||||
} else {
|
||||
this._hassOs = {};
|
||||
}
|
||||
}
|
||||
|
||||
_computeUpdateAvailable(data) {
|
||||
return data && data.version !== data.version_latest;
|
||||
}
|
||||
|
||||
_featureAvailable(data, feature) {
|
||||
return data && data.features && data.features.includes(feature);
|
||||
}
|
||||
|
||||
_showHardware() {
|
||||
this.hass.callApi('get', 'hassio/hardware/info')
|
||||
.then(
|
||||
resp => this._objectToMarkdown(resp.data)
|
||||
, () => 'Error getting hardware info'
|
||||
).then((content) => {
|
||||
this.fire('hassio-markdown-dialog', {
|
||||
title: 'Hardware',
|
||||
content: content,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_objectToMarkdown(obj, indent = '') {
|
||||
let data = '';
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (typeof obj[key] !== 'object') {
|
||||
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;
|
||||
}
|
||||
|
||||
_changeHostnameClicked() {
|
||||
const curHostname = this.data.hostname;
|
||||
const hostname = prompt('Please enter a new hostname:', curHostname);
|
||||
if (hostname && hostname !== curHostname) {
|
||||
this.hass.callApi('post', 'hassio/host/options', { hostname });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-host-info', HassioHostInfo);
|
153
hassio/src/system/hassio-supervisor-info.js
Normal file
153
hassio/src/system/hassio-supervisor-info.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import '@polymer/paper-button/paper-button.js';
|
||||
import '@polymer/paper-card/paper-card.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../../../src/components/buttons/ha-call-api-button.js';
|
||||
import EventsMixin from '../../../src/mixins/events-mixin.js';
|
||||
|
||||
class HassioSupervisorInfo extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
paper-card {
|
||||
display: inline-block;
|
||||
width: 400px;
|
||||
}
|
||||
.card-content {
|
||||
height: 200px;
|
||||
}
|
||||
@media screen and (max-width: 830px) {
|
||||
paper-card {
|
||||
width: 100%;
|
||||
}
|
||||
.card-content {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.info {
|
||||
width: 100%;
|
||||
}
|
||||
.info td:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
.errors {
|
||||
color: var(--google-red-500);
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<h2>Hass.io supervisor</h2>
|
||||
<table class="info">
|
||||
<tbody><tr>
|
||||
<td>Version</td>
|
||||
<td>
|
||||
[[data.version]]
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Latest version</td>
|
||||
<td>[[data.last_version]]</td>
|
||||
</tr>
|
||||
<template is="dom-if" if="[[!_equals(data.channel, "stable")]]">
|
||||
<tr>
|
||||
<td>Channel</td>
|
||||
<td>[[data.channel]]</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody></table>
|
||||
<template is="dom-if" if="[[errors]]">
|
||||
<div class="errors">Error: [[errors]]</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button hass="[[hass]]" path="hassio/supervisor/reload">Reload</ha-call-api-button>
|
||||
<template is="dom-if" if="[[computeUpdateAvailable(data)]]">
|
||||
<ha-call-api-button hass="[[hass]]" path="hassio/supervisor/update">Update</ha-call-api-button>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(data.channel, "beta")]]">
|
||||
<ha-call-api-button hass="[[hass]]" path="hassio/supervisor/options" data="[[leaveBeta]]">Leave beta channel</ha-call-api-button>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(data.channel, "stable")]]">
|
||||
<paper-button on-click="_joinBeta" class="warning" title="Get beta updates for Home Assistant (RCs), supervisor and host">Join beta channel</paper-button>
|
||||
</template>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
data: Object,
|
||||
errors: String,
|
||||
leaveBeta: {
|
||||
type: Object,
|
||||
value: { channel: 'stable' },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener('hass-api-called', ev => this.apiCalled(ev));
|
||||
}
|
||||
|
||||
apiCalled(ev) {
|
||||
if (ev.detail.success) {
|
||||
this.errors = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var response = ev.detail.response;
|
||||
|
||||
if (typeof response.body === 'object') {
|
||||
this.errors = response.body.message || 'Unknown error';
|
||||
} else {
|
||||
this.errors = response.body;
|
||||
}
|
||||
}
|
||||
|
||||
computeUpdateAvailable(data) {
|
||||
return data.version !== data.last_version;
|
||||
}
|
||||
|
||||
_equals(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
_joinBeta() {
|
||||
if (!confirm(`WARNING:
|
||||
Beta releases are for testers and early adopters and can contain unstable code changes. Make sure you have backups of your data before you activate this feature.
|
||||
|
||||
This inludes beta releases for:
|
||||
- Home Assistant (Release Candidates)
|
||||
- Hass.io supervisor
|
||||
- Host system`)) {
|
||||
return;
|
||||
}
|
||||
const method = 'post';
|
||||
const path = 'hassio/supervisor/options';
|
||||
const data = { channel: 'beta' };
|
||||
|
||||
const eventData = {
|
||||
method: method,
|
||||
path: path,
|
||||
data: data,
|
||||
};
|
||||
|
||||
this.hass.callApi(method, path, data)
|
||||
.then((resp) => {
|
||||
eventData.success = true;
|
||||
eventData.response = resp;
|
||||
}, (resp) => {
|
||||
eventData.success = false;
|
||||
eventData.response = resp;
|
||||
}).then(() => {
|
||||
this.fire('hass-api-called', eventData);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-supervisor-info', HassioSupervisorInfo);
|
54
hassio/src/system/hassio-supervisor-log.js
Normal file
54
hassio/src/system/hassio-supervisor-log.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import '@polymer/paper-button/paper-button.js';
|
||||
import '@polymer/paper-card/paper-card.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
class HassioSupervisorLog extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style">
|
||||
paper-card {
|
||||
display: block;
|
||||
}
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<pre>[[log]]</pre>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<paper-button on-click="refreshTapped">Refresh</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
log: String,
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
loadData() {
|
||||
this.hass.callApi('get', 'hassio/supervisor/logs')
|
||||
.then((info) => {
|
||||
this.log = info;
|
||||
}, () => {
|
||||
this.log = 'Error fetching logs';
|
||||
});
|
||||
}
|
||||
|
||||
refreshTapped() {
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-supervisor-log', HassioSupervisorLog);
|
43
hassio/src/system/hassio-system.js
Normal file
43
hassio/src/system/hassio-system.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import '@polymer/iron-flex-layout/iron-flex-layout-classes.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import './hassio-host-info.js';
|
||||
import './hassio-supervisor-info.js';
|
||||
import './hassio-supervisor-log.js';
|
||||
|
||||
class HassioSystem extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
.content {
|
||||
margin: 4px;
|
||||
}
|
||||
.title {
|
||||
margin-top: 24px;
|
||||
color: var(--primary-text-color);
|
||||
font-size: 2em;
|
||||
padding-left: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
<div class="content">
|
||||
<div class="title">Information</div>
|
||||
<hassio-supervisor-info hass="[[hass]]" data="[[supervisorInfo]]"></hassio-supervisor-info>
|
||||
<hassio-host-info hass="[[hass]]" data="[[hostInfo]]"></hassio-host-info>
|
||||
<div class="title">System log</div>
|
||||
<hassio-supervisor-log hass="[[hass]]"></hassio-supervisor-log>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
supervisorInfo: Object,
|
||||
hostInfo: Object,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hassio-system', HassioSystem);
|
88
hassio/webpack.config.js
Normal file
88
hassio/webpack.config.js
Normal file
@@ -0,0 +1,88 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||
const CompressionPlugin = require("compression-webpack-plugin");
|
||||
const config = require('./config.js');
|
||||
|
||||
const version = fs.readFileSync('../setup.py', 'utf8').match(/\d{8}[^']*/);
|
||||
if (!version) {
|
||||
throw Error('Version not found');
|
||||
}
|
||||
const VERSION = version[0];
|
||||
const isProdBuild = process.env.NODE_ENV === 'production'
|
||||
const chunkFilename = isProdBuild ?
|
||||
'chunk.[chunkhash].js' : '[name].chunk.js';
|
||||
|
||||
const plugins = [
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: JSON.stringify(!isProdBuild),
|
||||
__VERSION__: JSON.stringify(VERSION),
|
||||
})
|
||||
];
|
||||
|
||||
if (isProdBuild) {
|
||||
plugins.push(new UglifyJsPlugin({
|
||||
extractComments: true,
|
||||
sourceMap: true,
|
||||
uglifyOptions: {
|
||||
// Disabling because it broke output
|
||||
mangle: false,
|
||||
}
|
||||
}));
|
||||
plugins.push(new CompressionPlugin({
|
||||
cache: true,
|
||||
exclude: [
|
||||
/\.js\.map$/,
|
||||
/\.LICENSE$/,
|
||||
/\.py$/,
|
||||
/\.txt$/,
|
||||
]
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mode: isProdBuild ? 'production' : 'development',
|
||||
// Disabled in prod while we make Home Assistant able to serve the right files.
|
||||
// Was source-map
|
||||
devtool: isProdBuild ? 'none' : 'inline-source-map',
|
||||
entry: {
|
||||
entrypoint: './src/entrypoint.js',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
[require('babel-preset-env').default, { modules: false }]
|
||||
],
|
||||
plugins: [
|
||||
// Only support the syntax, Webpack will handle it.
|
||||
"syntax-dynamic-import",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(html)$/,
|
||||
use: {
|
||||
loader: 'html-loader',
|
||||
options: {
|
||||
exportAsEs6Default: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
plugins,
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
chunkFilename: chunkFilename,
|
||||
path: config.buildDir,
|
||||
publicPath: `${config.publicPath}/`,
|
||||
}
|
||||
};
|
78
hassio/webpack.legacy.config.js
Normal file
78
hassio/webpack.legacy.config.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||
const config = require('./config.js');
|
||||
|
||||
const version = fs.readFileSync('../setup.py', 'utf8').match(/\d{8}[^']*/);
|
||||
if (!version) {
|
||||
throw Error('Version not found');
|
||||
}
|
||||
const VERSION = version[0];
|
||||
const isProdBuild = process.env.NODE_ENV === 'production'
|
||||
const chunkFilename = isProdBuild ?
|
||||
'chunk.[chunkhash].js' : '[name].chunk.js';
|
||||
|
||||
const plugins = [
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: JSON.stringify(!isProdBuild),
|
||||
__VERSION__: JSON.stringify(VERSION),
|
||||
})
|
||||
];
|
||||
|
||||
if (isProdBuild) {
|
||||
plugins.push(new UglifyJsPlugin({
|
||||
extractComments: true,
|
||||
sourceMap: true,
|
||||
uglifyOptions: {
|
||||
// Disabling because it broke output
|
||||
mangle: false,
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mode: isProdBuild ? 'production' : 'development',
|
||||
// Disabled in prod while we make Home Assistant able to serve the right files.
|
||||
// Was source-map
|
||||
devtool: isProdBuild ? 'none' : 'inline-source-map',
|
||||
entry: {
|
||||
app: './src/hassio-app.js',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
[require('babel-preset-env').default, { modules: false }]
|
||||
],
|
||||
plugins: [
|
||||
// Only support the syntax, Webpack will handle it.
|
||||
"syntax-dynamic-import",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(html)$/,
|
||||
use: {
|
||||
loader: 'html-loader',
|
||||
options: {
|
||||
exportAsEs6Default: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
plugins,
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
chunkFilename: chunkFilename,
|
||||
path: config.buildDirLegacy,
|
||||
publicPath: `${config.publicPathLegacy}/`,
|
||||
}
|
||||
};
|
102
index.html
Normal file
102
index.html
Normal file
@@ -0,0 +1,102 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Home Assistant</title>
|
||||
|
||||
<link rel='manifest' href='/manifest.json' crossorigin="use-credentials">
|
||||
<link rel='icon' href='/static/icons/favicon.ico'>
|
||||
<link rel='apple-touch-icon' sizes='180x180'
|
||||
href='/static/icons/favicon-apple-180x180.png'>
|
||||
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#3fbbf4">
|
||||
<link rel='preload' href='/frontend_latest/core.js' as='script'/>
|
||||
<link rel='preload' href='/static/fonts/roboto/Roboto-Regular.ttf' as='font' crossorigin />
|
||||
<link rel='preload' href='/static/fonts/roboto/Roboto-Medium.ttf' as='font' crossorigin />
|
||||
<meta name='apple-mobile-web-app-capable' content='yes'>
|
||||
<meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/>
|
||||
<meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/>
|
||||
<meta name="msapplication-wide310x150logo" content="/static/icons/tile-win-310x150.png"/>
|
||||
<meta name="msapplication-square310x310logo" content="/static/icons/tile-win-310x310.png"/>
|
||||
<meta name="msapplication-TileColor" content="#3fbbf4ff"/>
|
||||
<meta name='mobile-web-app-capable' content='yes'>
|
||||
<meta name='referrer' content='same-origin'>
|
||||
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
||||
<meta name='theme-color' content='{{ theme_color }}'>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Roboto', 'Noto', sans-serif;
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#ha-init-skeleton::before {
|
||||
display: block;
|
||||
content: "";
|
||||
height: 64px;
|
||||
background-color: {{ theme_color }};
|
||||
}
|
||||
|
||||
#ha-init-skeleton .message {
|
||||
transition: font-size 2s;
|
||||
font-size: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
#ha-init-skeleton.error .message {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#ha-init-skeleton a {
|
||||
color: {{ theme_color }};
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function initError() {
|
||||
document.getElementById('ha-init-skeleton').classList.add('error');
|
||||
};
|
||||
window.noAuth = '{{ no_auth }}';
|
||||
window.useOAuth = '{{ use_oauth }}'
|
||||
window.Polymer = {
|
||||
lazyRegister: true,
|
||||
useNativeCSSProperties: true,
|
||||
dom: 'shadow',
|
||||
suppressTemplateNotifications: true,
|
||||
suppressBindingNotifications: true,
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='ha-init-skeleton'>
|
||||
<div class='message'>
|
||||
Home Assistant had trouble<br>connecting to the server.<br><br>
|
||||
<a href='/'>TRY AGAIN</a>
|
||||
</div>
|
||||
</div>
|
||||
<home-assistant></home-assistant>
|
||||
<script>
|
||||
var webComponentsSupported = (
|
||||
'customElements' in window &&
|
||||
'content' in document.createElement('template'));
|
||||
if (!webComponentsSupported) {
|
||||
(function() {
|
||||
var e = document.createElement('script');
|
||||
e.src = '/static/webcomponents-bundle.js';
|
||||
document.write(e.outerHTML);
|
||||
}());
|
||||
}
|
||||
</script>
|
||||
<!--EXTRA_SCRIPTS-->
|
||||
<script src='/frontend_latest/core.js'></script>
|
||||
<script src='/frontend_latest/app.js'></script>
|
||||
<script src='/frontend_latest/hass-icons.js' async></script>
|
||||
{% for extra_url in extra_urls -%}
|
||||
<link rel='import' href='{{ extra_url }}' async>
|
||||
{% endfor -%}
|
||||
</body>
|
||||
</html>
|
@@ -1,10 +0,0 @@
|
||||
import { h, render } from 'preact';
|
||||
import Automation from './automation';
|
||||
|
||||
window.AutomationEditor = function (mountEl, props, mergeEl) {
|
||||
return render(h(Automation, props), mountEl, mergeEl);
|
||||
};
|
||||
|
||||
window.unmountPreact = function (mountEl, mergeEl) {
|
||||
render(() => null, mountEl, mergeEl);
|
||||
};
|
@@ -1,112 +0,0 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import Trigger from './trigger';
|
||||
import Condition from '../common/component/condition';
|
||||
import Script from '../common/component/script';
|
||||
|
||||
export default class Automation extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.triggerChanged = this.triggerChanged.bind(this);
|
||||
this.conditionChanged = this.conditionChanged.bind(this);
|
||||
this.actionChanged = this.actionChanged.bind(this);
|
||||
}
|
||||
|
||||
onChange(ev) {
|
||||
this.props.onChange({
|
||||
...this.props.automation,
|
||||
[ev.target.name]: ev.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
triggerChanged(trigger) {
|
||||
this.props.onChange({
|
||||
...this.props.automation,
|
||||
trigger,
|
||||
});
|
||||
}
|
||||
|
||||
conditionChanged(condition) {
|
||||
this.props.onChange({
|
||||
...this.props.automation,
|
||||
condition,
|
||||
});
|
||||
}
|
||||
|
||||
actionChanged(action) {
|
||||
this.props.onChange({
|
||||
...this.props.automation,
|
||||
action,
|
||||
});
|
||||
}
|
||||
|
||||
render({ automation, isWide }) {
|
||||
const {
|
||||
alias, trigger, condition, action
|
||||
} = automation;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ha-config-section is-wide={isWide}>
|
||||
<span slot='header'>{alias}</span>
|
||||
<span slot='introduction'>
|
||||
Use automations to bring your home alive.
|
||||
</span>
|
||||
<paper-card>
|
||||
<div class='card-content'>
|
||||
<paper-input
|
||||
label="Name"
|
||||
name="alias"
|
||||
value={alias}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
</paper-card>
|
||||
</ha-config-section>
|
||||
|
||||
<ha-config-section is-wide={isWide}>
|
||||
<span slot='header'>Triggers</span>
|
||||
<span slot='introduction'>
|
||||
Triggers are what starts the processing of an automation rule.
|
||||
It is possible to specify multiple triggers for the same rule.
|
||||
Once a trigger starts, Home Assistant will validate the conditions,
|
||||
if any, and call the action.
|
||||
<p><a href="https://home-assistant.io/docs/automation/trigger/" target="_blank">
|
||||
Learn more about triggers.
|
||||
</a></p>
|
||||
</span>
|
||||
<Trigger trigger={trigger} onChange={this.triggerChanged} />
|
||||
</ha-config-section>
|
||||
|
||||
<ha-config-section is-wide={isWide}>
|
||||
<span slot='header'>Conditions</span>
|
||||
<span slot='introduction'>
|
||||
Conditions are an optional part of an automation rule and can be used to prevent
|
||||
an action from happening when triggered. Conditions look very similar to triggers
|
||||
but are very different. A trigger will look at events happening in the system
|
||||
while a condition only looks at how the system looks right now. A trigger can
|
||||
observe that a switch is being turned on. A condition can only see if a switch
|
||||
is currently on or off.
|
||||
<p><a href="https://home-assistant.io/docs/scripts/conditions/" target="_blank">
|
||||
Learn more about conditions.
|
||||
</a></p>
|
||||
</span>
|
||||
<Condition condition={condition || []} onChange={this.conditionChanged} />
|
||||
</ha-config-section>
|
||||
|
||||
<ha-config-section is-wide={isWide}>
|
||||
<span slot='header'>Action</span>
|
||||
<span slot='introduction'>
|
||||
The actions are what Home Assistant will do when the automation is triggered.
|
||||
<p><a href="https://home-assistant.io/docs/scripts/" target="_blank">
|
||||
Learn more about actions.
|
||||
</a></p>
|
||||
</span>
|
||||
<Script script={action} onChange={this.actionChanged} />
|
||||
</ha-config-section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,38 +0,0 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
export default class HassTrigger extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.radioGroupPicked = this.radioGroupPicked.bind(this);
|
||||
}
|
||||
|
||||
radioGroupPicked(ev) {
|
||||
this.props.onChange(this.props.index, {
|
||||
...this.props.trigger,
|
||||
event: ev.target.selected,
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
render({ trigger }) {
|
||||
const { event } = trigger;
|
||||
return (
|
||||
<div>
|
||||
<label id="eventlabel">Event:</label>
|
||||
<paper-radio-group
|
||||
selected={event}
|
||||
aria-labelledby="eventlabel"
|
||||
onpaper-radio-group-changed={this.radioGroupPicked}
|
||||
>
|
||||
<paper-radio-button name="start">Start</paper-radio-button>
|
||||
<paper-radio-button name="shutdown">Shutdown</paper-radio-button>
|
||||
</paper-radio-group>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HassTrigger.defaultConfig = {
|
||||
event: 'start'
|
||||
};
|
@@ -1,50 +0,0 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { onChangeEvent } from '../../common/util/event';
|
||||
|
||||
export default class NumericStateTrigger extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onChange = onChangeEvent.bind(this, 'trigger');
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
render({ trigger }) {
|
||||
const {
|
||||
value_template, entity_id, below, above
|
||||
} = trigger;
|
||||
return (
|
||||
<div>
|
||||
<paper-input
|
||||
label="Entity Id"
|
||||
name="entity_id"
|
||||
value={entity_id}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<paper-input
|
||||
label="Above"
|
||||
name="above"
|
||||
value={above}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<paper-input
|
||||
label="Below"
|
||||
name="below"
|
||||
value={below}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<paper-textarea
|
||||
label="Value template (optional)"
|
||||
name="value_template"
|
||||
value={value_template}
|
||||
onvalue-changed={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NumericStateTrigger.defaultConfig = {
|
||||
entity_id: '',
|
||||
};
|
@@ -1,45 +0,0 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { onChangeEvent } from '../../common/util/event';
|
||||
|
||||
export default class StateTrigger extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onChange = onChangeEvent.bind(this, 'trigger');
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
render({ trigger }) {
|
||||
const { entity_id, to } = trigger;
|
||||
const trgFrom = trigger.from;
|
||||
const trgFor = trigger.for;
|
||||
return (
|
||||
<div>
|
||||
<paper-input
|
||||
label="Entity Id"
|
||||
name="entity_id"
|
||||
value={entity_id}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<paper-input
|
||||
label="From"
|
||||
name="from"
|
||||
value={trgFrom}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<paper-input
|
||||
label="To"
|
||||
name="to"
|
||||
value={to}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
{trgFor && <pre>For: {JSON.stringify(trgFor, null, 2)}</pre>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StateTrigger.defaultConfig = {
|
||||
entity_id: '',
|
||||
};
|
@@ -1,48 +0,0 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { onChangeEvent } from '../../common/util/event';
|
||||
|
||||
export default class SunTrigger extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onChange = onChangeEvent.bind(this, 'trigger');
|
||||
this.radioGroupPicked = this.radioGroupPicked.bind(this);
|
||||
}
|
||||
|
||||
radioGroupPicked(ev) {
|
||||
this.props.onChange(this.props.index, {
|
||||
...this.props.trigger,
|
||||
event: ev.target.selected,
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
render({ trigger }) {
|
||||
const { offset, event } = trigger;
|
||||
return (
|
||||
<div>
|
||||
<label id="eventlabel">Event:</label>
|
||||
<paper-radio-group
|
||||
selected={event}
|
||||
aria-labelledby="eventlabel"
|
||||
onpaper-radio-group-changed={this.radioGroupPicked}
|
||||
>
|
||||
<paper-radio-button name="sunrise">Sunrise</paper-radio-button>
|
||||
<paper-radio-button name="sunset">Sunset</paper-radio-button>
|
||||
</paper-radio-group>
|
||||
|
||||
<paper-input
|
||||
label="Offset (optional)"
|
||||
name="offset"
|
||||
value={offset}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SunTrigger.defaultConfig = {
|
||||
event: 'sunrise',
|
||||
};
|
@@ -1,76 +0,0 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import EventTrigger from './event';
|
||||
import HassTrigger from './homeassistant';
|
||||
import MQTTTrigger from './mqtt';
|
||||
import NumericStateTrigger from './numeric_state';
|
||||
import StateTrigger from './state';
|
||||
import SunTrigger from './sun';
|
||||
import TemplateTrigger from './template';
|
||||
import TimeTrigger from './time';
|
||||
import ZoneTrigger from './zone';
|
||||
|
||||
const TYPES = {
|
||||
event: EventTrigger,
|
||||
state: StateTrigger,
|
||||
homeassistant: HassTrigger,
|
||||
mqtt: MQTTTrigger,
|
||||
numeric_state: NumericStateTrigger,
|
||||
sun: SunTrigger,
|
||||
template: TemplateTrigger,
|
||||
time: TimeTrigger,
|
||||
zone: ZoneTrigger,
|
||||
};
|
||||
|
||||
const OPTIONS = Object.keys(TYPES).sort();
|
||||
|
||||
export default class TriggerEdit extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.typeChanged = this.typeChanged.bind(this);
|
||||
}
|
||||
|
||||
typeChanged(ev) {
|
||||
const type = ev.target.selectedItem.innerHTML;
|
||||
|
||||
if (type !== this.props.trigger.platform) {
|
||||
this.props.onChange(this.props.index, {
|
||||
platform: type,
|
||||
...TYPES[type].defaultConfig
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render({ index, trigger, onChange }) {
|
||||
const Comp = TYPES[trigger.platform];
|
||||
const selected = OPTIONS.indexOf(trigger.platform);
|
||||
|
||||
if (!Comp) {
|
||||
return (
|
||||
<div>
|
||||
Unsupported platform: {trigger.platform}
|
||||
<pre>{JSON.stringify(trigger, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<paper-dropdown-menu-light label="Trigger Type" no-animations>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
selected={selected}
|
||||
oniron-select={this.typeChanged}
|
||||
>
|
||||
{OPTIONS.map(opt => <paper-item>{opt}</paper-item>)}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu-light>
|
||||
<Comp
|
||||
index={index}
|
||||
trigger={trigger}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,55 +0,0 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { onChangeEvent } from '../../common/util/event';
|
||||
|
||||
export default class ZoneTrigger extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onChange = onChangeEvent.bind(this, 'trigger');
|
||||
this.radioGroupPicked = this.radioGroupPicked.bind(this);
|
||||
}
|
||||
|
||||
radioGroupPicked(ev) {
|
||||
this.props.onChange(this.props.index, {
|
||||
...this.props.trigger,
|
||||
event: ev.target.selected,
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
render({ trigger }) {
|
||||
const { entity_id, zone, event } = trigger;
|
||||
return (
|
||||
<div>
|
||||
<paper-input
|
||||
label="Entity Id"
|
||||
name="entity_id"
|
||||
value={entity_id}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<paper-input
|
||||
label="Zone"
|
||||
name="zone"
|
||||
value={zone}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<label id="eventlabel">Event:</label>
|
||||
<paper-radio-group
|
||||
selected={event}
|
||||
aria-labelledby="eventlabel"
|
||||
onpaper-radio-group-changed={this.radioGroupPicked}
|
||||
>
|
||||
<paper-radio-button name="enter">Enter</paper-radio-button>
|
||||
<paper-radio-button name="leave">Leave</paper-radio-button>
|
||||
</paper-radio-group>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ZoneTrigger.defaultConfig = {
|
||||
entity_id: '',
|
||||
zone: '',
|
||||
event: 'enter',
|
||||
};
|
@@ -1,71 +0,0 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import NumericStateCondition from './numeric_state';
|
||||
import StateCondition from './state';
|
||||
import SunCondition from './sun';
|
||||
import TemplateCondition from './template';
|
||||
import TimeCondition from './time';
|
||||
import ZoneCondition from './zone';
|
||||
|
||||
const TYPES = {
|
||||
state: StateCondition,
|
||||
numeric_state: NumericStateCondition,
|
||||
sun: SunCondition,
|
||||
template: TemplateCondition,
|
||||
time: TimeCondition,
|
||||
zone: ZoneCondition,
|
||||
};
|
||||
|
||||
const OPTIONS = Object.keys(TYPES).sort();
|
||||
|
||||
export default class ConditionRow extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.typeChanged = this.typeChanged.bind(this);
|
||||
}
|
||||
|
||||
typeChanged(ev) {
|
||||
const type = ev.target.selectedItem.innerHTML;
|
||||
|
||||
if (type !== this.props.condition.condition) {
|
||||
this.props.onChange(this.props.index, {
|
||||
condition: type,
|
||||
...TYPES[type].defaultConfig
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render({ index, condition, onChange }) {
|
||||
const Comp = TYPES[condition.condition];
|
||||
const selected = OPTIONS.indexOf(condition.condition);
|
||||
|
||||
if (!Comp) {
|
||||
return (
|
||||
<div>
|
||||
Unsupported condition: {condition.condition}
|
||||
<pre>{JSON.stringify(condition, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<paper-dropdown-menu-light label="Condition Type" no-animations>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
selected={selected}
|
||||
oniron-select={this.typeChanged}
|
||||
>
|
||||
{OPTIONS.map(opt => <paper-item>{opt}</paper-item>)}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu-light>
|
||||
<Comp
|
||||
index={index}
|
||||
condition={condition}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { onChangeEvent } from '../../util/event';
|
||||
|
||||
export default class NumericStateCondition extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onChange = onChangeEvent.bind(this, 'condition');
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
render({ condition }) {
|
||||
const {
|
||||
value_template, entity_id, below, above
|
||||
} = condition;
|
||||
return (
|
||||
<div>
|
||||
<paper-input
|
||||
label="Entity Id"
|
||||
name="entity_id"
|
||||
value={entity_id}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<paper-input
|
||||
label="Above"
|
||||
name="above"
|
||||
value={above}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<paper-input
|
||||
label="Below"
|
||||
name="below"
|
||||
value={below}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<paper-textarea
|
||||
label="Value template (optional)"
|
||||
name="value_template"
|
||||
value={value_template}
|
||||
onvalue-changed={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NumericStateCondition.defaultConfig = {
|
||||
entity_id: '',
|
||||
};
|
@@ -1,39 +0,0 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { onChangeEvent } from '../../util/event';
|
||||
|
||||
export default class StateCondition extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onChange = onChangeEvent.bind(this, 'condition');
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
render({ condition }) {
|
||||
const { entity_id, state } = condition;
|
||||
const cndFor = condition.for;
|
||||
return (
|
||||
<div>
|
||||
<paper-input
|
||||
label="Entity Id"
|
||||
name="entity_id"
|
||||
value={entity_id}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<paper-input
|
||||
label="State"
|
||||
name="state"
|
||||
value={state}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
{cndFor && <pre>For: {JSON.stringify(cndFor, null, 2)}</pre>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StateCondition.defaultConfig = {
|
||||
entity_id: '',
|
||||
state: '',
|
||||
};
|
@@ -1,37 +0,0 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { onChangeEvent } from '../../util/event';
|
||||
|
||||
export default class ZoneCondition extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onChange = onChangeEvent.bind(this, 'condition');
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
render({ condition }) {
|
||||
const { entity_id, zone } = condition;
|
||||
return (
|
||||
<div>
|
||||
<paper-input
|
||||
label="Entity Id"
|
||||
name="entity_id"
|
||||
value={entity_id}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<paper-input
|
||||
label="Zone entity id"
|
||||
name="zone"
|
||||
value={zone}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ZoneCondition.defaultConfig = {
|
||||
entity_id: '',
|
||||
zone: '',
|
||||
};
|
@@ -1,52 +0,0 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import JSONTextArea from '../json_textarea';
|
||||
import { onChangeEvent } from '../../util/event';
|
||||
|
||||
export default class CallServiceAction extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onChange = onChangeEvent.bind(this, 'action');
|
||||
this.serviceDataChanged = this.serviceDataChanged.bind(this);
|
||||
}
|
||||
|
||||
serviceDataChanged(data) {
|
||||
this.props.onChange(this.props.index, {
|
||||
...this.props.action,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
render({ action }) {
|
||||
const { alias, service, data } = action;
|
||||
return (
|
||||
<div>
|
||||
<paper-input
|
||||
label="Alias"
|
||||
name="alias"
|
||||
value={alias}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<paper-input
|
||||
label="Service"
|
||||
name="service"
|
||||
value={service}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<JSONTextArea
|
||||
label="Service Data"
|
||||
value={data}
|
||||
onChange={this.serviceDataChanged}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CallServiceAction.configKey = 'service';
|
||||
CallServiceAction.defaultConfig = {
|
||||
alias: '',
|
||||
service: '',
|
||||
data: {}
|
||||
};
|
@@ -1,23 +0,0 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import StateCondition from '../condition/state';
|
||||
import ConditionEdit from '../condition/condition_edit';
|
||||
|
||||
export default class ConditionAction extends Component {
|
||||
// eslint-disable-next-line
|
||||
render({ action, index, onChange }) {
|
||||
return (
|
||||
<ConditionEdit
|
||||
condition={action}
|
||||
onChange={onChange}
|
||||
index={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ConditionAction.configKey = 'condition';
|
||||
ConditionAction.defaultConfig = {
|
||||
condition: 'state',
|
||||
...StateCondition.defaultConfig,
|
||||
};
|
@@ -1,3 +0,0 @@
|
||||
export function validEntityId(entityId) {
|
||||
return /^(\w+)\.(\w+)$/.test(entityId);
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
import objAssign from 'es6-object-assign';
|
||||
|
||||
objAssign.polyfill();
|
37
js/core.js
37
js/core.js
@@ -1,37 +0,0 @@
|
||||
import * as HAWS from 'home-assistant-js-websocket';
|
||||
|
||||
window.HAWS = HAWS;
|
||||
window.HASS_DEMO = __DEMO__;
|
||||
window.HASS_DEV = __DEV__;
|
||||
|
||||
const init = window.createHassConnection = function (password) {
|
||||
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const url = `${proto}://${window.location.host}/api/websocket`;
|
||||
const options = {
|
||||
setupRetry: 10,
|
||||
};
|
||||
if (password !== undefined) {
|
||||
options.authToken = password;
|
||||
}
|
||||
|
||||
return HAWS.createConnection(url, options)
|
||||
.then(function (conn) {
|
||||
HAWS.subscribeEntities(conn);
|
||||
HAWS.subscribeConfig(conn);
|
||||
return conn;
|
||||
});
|
||||
};
|
||||
|
||||
if (window.noAuth) {
|
||||
window.hassConnection = init();
|
||||
} else if (window.localStorage.authToken) {
|
||||
window.hassConnection = init(window.localStorage.authToken);
|
||||
} else {
|
||||
window.hassConnection = null;
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/service_worker.js');
|
||||
});
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
import { h, render } from 'preact';
|
||||
import Script from './script';
|
||||
|
||||
window.ScriptEditor = function (mountEl, props, mergeEl) {
|
||||
return render(h(Script, props), mountEl, mergeEl);
|
||||
};
|
||||
|
||||
window.unmountPreact = function (mountEl, mergeEl) {
|
||||
render(() => null, mountEl, mergeEl);
|
||||
};
|
186
package.json
186
package.json
@@ -1,79 +1,149 @@
|
||||
{
|
||||
"name": "home-assistant-polymer",
|
||||
"version": "1.0.0",
|
||||
"description": "A frontend for Home Assistant using the Polymer framework",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/home-assistant/home-assistant-polymer"
|
||||
},
|
||||
"name": "home-assistant-frontend",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"clean": "rm -rf build/* build-temp/*",
|
||||
"gulp": "gulp",
|
||||
"build": "BUILD_DEV=0 gulp",
|
||||
"build_demo": "BUILD_DEV=0 BUILD_DEMO=1 gulp",
|
||||
"dev": "npm run gulp ru_all gen-service-worker",
|
||||
"dev-watch": "npm run gulp watch_ru_all gen-service-worker",
|
||||
"lint_js": "eslint src panels js --ext js,html",
|
||||
"lint_html": "ls -1 src/home-assistant.html panels/**/ha-panel-*.html | xargs polymer lint --input",
|
||||
"test": "npm run lint_js && npm run lint_html"
|
||||
"build": "script/build_frontend",
|
||||
"lint": "eslint src hassio/src test-mocha && polymer lint",
|
||||
"mocha": "node_modules/.bin/mocha --opts test-mocha/mocha.opts",
|
||||
"test": "npm run lint && npm run mocha"
|
||||
},
|
||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-plugin-transform-react-jsx": "^6.24.1",
|
||||
"babel-preset-babili": "^0.1.4",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"bower": "^1.8.2",
|
||||
"css-slam": "^2.0.2",
|
||||
"del": "^3.0.0",
|
||||
"@mdi/svg": "^2.4.85",
|
||||
"@polymer/app-layout": "^3.0.0-pre.19",
|
||||
"@polymer/app-localize-behavior": "^3.0.0-pre.19",
|
||||
"@polymer/app-route": "^3.0.0-pre.19",
|
||||
"@polymer/app-storage": "^3.0.0-pre.19",
|
||||
"@polymer/font-roboto": "^3.0.0-pre.19",
|
||||
"@polymer/font-roboto-local": "^3.0.0-pre.19",
|
||||
"@polymer/iron-autogrow-textarea": "^3.0.0-pre.19",
|
||||
"@polymer/iron-flex-layout": "^3.0.0-pre.19",
|
||||
"@polymer/iron-icon": "^3.0.0-pre.19",
|
||||
"@polymer/iron-iconset-svg": "^3.0.0-pre.19",
|
||||
"@polymer/iron-image": "^3.0.0-pre.19",
|
||||
"@polymer/iron-input": "^3.0.0-pre.19",
|
||||
"@polymer/iron-label": "^3.0.0-pre.19",
|
||||
"@polymer/iron-media-query": "^3.0.0-pre.19",
|
||||
"@polymer/iron-pages": "^3.0.0-pre.19",
|
||||
"@polymer/iron-resizable-behavior": "^3.0.0-pre.19",
|
||||
"@polymer/neon-animation": "^3.0.0-pre.19",
|
||||
"@polymer/paper-button": "^3.0.0-pre.19",
|
||||
"@polymer/paper-card": "^3.0.0-pre.19",
|
||||
"@polymer/paper-checkbox": "^3.0.0-pre.19",
|
||||
"@polymer/paper-dialog": "^3.0.0-pre.19",
|
||||
"@polymer/paper-dialog-behavior": "^3.0.0-pre.19",
|
||||
"@polymer/paper-dialog-scrollable": "^3.0.0-pre.19",
|
||||
"@polymer/paper-drawer-panel": "^3.0.0-pre.19",
|
||||
"@polymer/paper-dropdown-menu": "^3.0.0-pre.19",
|
||||
"@polymer/paper-fab": "^3.0.0-pre.19",
|
||||
"@polymer/paper-icon-button": "^3.0.0-pre.19",
|
||||
"@polymer/paper-input": "^3.0.0-pre.19",
|
||||
"@polymer/paper-item": "^3.0.0-pre.19",
|
||||
"@polymer/paper-listbox": "^3.0.0-pre.19",
|
||||
"@polymer/paper-menu-button": "^3.0.0-pre.19",
|
||||
"@polymer/paper-progress": "^3.0.0-pre.19",
|
||||
"@polymer/paper-radio-button": "^3.0.0-pre.19",
|
||||
"@polymer/paper-radio-group": "^3.0.0-pre.19",
|
||||
"@polymer/paper-ripple": "^3.0.0-pre.19",
|
||||
"@polymer/paper-scroll-header-panel": "^3.0.0-pre.19",
|
||||
"@polymer/paper-slider": "^3.0.0-pre.19",
|
||||
"@polymer/paper-spinner": "^3.0.0-pre.19",
|
||||
"@polymer/paper-styles": "^3.0.0-pre.19",
|
||||
"@polymer/paper-tabs": "^3.0.0-pre.19",
|
||||
"@polymer/paper-toast": "^3.0.0-pre.19",
|
||||
"@polymer/paper-toggle-button": "^3.0.0-pre.19",
|
||||
"@polymer/polymer": "^3.0.2",
|
||||
"@vaadin/vaadin-combo-box": "4.1.0-alpha2",
|
||||
"@vaadin/vaadin-date-picker": "3.2.0-alpha3",
|
||||
"@webcomponents/shadycss": "^1.3.1",
|
||||
"@webcomponents/webcomponentsjs": "^2.0.2",
|
||||
"chart.js": "~2.7.2",
|
||||
"chartjs-chart-timeline": "^0.2.1",
|
||||
"es6-object-assign": "^1.1.0",
|
||||
"eslint": "^4.8.0",
|
||||
"eslint-config-airbnb-base": "^12.0.2",
|
||||
"eslint-plugin-html": "^3.2.2",
|
||||
"eslint-plugin-import": "^2.2.0",
|
||||
"eslint-plugin-react": "^7.0.0",
|
||||
"eslint-import-resolver-webpack": "^0.10.0",
|
||||
"fecha": "^2.3.3",
|
||||
"home-assistant-js-websocket": "2.0.1",
|
||||
"intl-messageformat": "^2.2.0",
|
||||
"leaflet": "^1.3.1",
|
||||
"marked": "^0.4.0",
|
||||
"mdn-polyfills": "^5.8.0",
|
||||
"moment": "^2.22.2",
|
||||
"preact": "^8.2.9",
|
||||
"preact-compat": "^3.18.0",
|
||||
"react-big-calendar": "^0.19.1",
|
||||
"regenerator-runtime": "^0.11.1",
|
||||
"unfetch": "^3.0.0",
|
||||
"web-animations-js": "^2.3.1",
|
||||
"xss": "^1.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^8.2.3",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-react-jsx": "^6.24.1",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"chai": "^4.1.2",
|
||||
"compression-webpack-plugin": "^1.1.11",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"css-slam": "^2.1.2",
|
||||
"del": "^3.0.0",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-config-airbnb-base": "^12.1.0",
|
||||
"eslint-plugin-import": "^2.12.0",
|
||||
"eslint-plugin-react": "^7.9.1",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-babel": "^7.0.0",
|
||||
"gulp-file": "^0.3.0",
|
||||
"gulp-filter": "^5.0.1",
|
||||
"gulp-babel": "^7.0.1",
|
||||
"gulp-batch-replace": "^0.0.0",
|
||||
"gulp-foreach": "^0.1.0",
|
||||
"gulp-hash": "^4.0.1",
|
||||
"gulp-hash": "^4.2.2",
|
||||
"gulp-html-minifier": "^0.1.8",
|
||||
"gulp-if": "^2.0.2",
|
||||
"gulp-insert": "^0.5.0",
|
||||
"gulp-json-transform": "^0.4.2",
|
||||
"gulp-jsonminify": "^1.0.0",
|
||||
"gulp-merge-json": "^1.0.0",
|
||||
"gulp-rename": "^1.2.2",
|
||||
"gulp-rollup-each": "^2.0.0",
|
||||
"gulp-json-transform": "^0.4.5",
|
||||
"gulp-jsonminify": "^1.1.0",
|
||||
"gulp-merge-json": "^1.3.1",
|
||||
"gulp-rename": "^1.3.0",
|
||||
"gulp-uglify": "^3.0.0",
|
||||
"gulp-util": "^3.0.8",
|
||||
"home-assistant-js-websocket": "^1.1.0",
|
||||
"html-minifier": "^3.5.5",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-minifier": "^3.5.16",
|
||||
"merge-stream": "^1.0.1",
|
||||
"parse5": "^3.0.2",
|
||||
"polymer-analyzer": "^2.3.0",
|
||||
"polymer-build": "^2.1.0",
|
||||
"polymer-bundler": "^3.1.0",
|
||||
"polymer-cli": "^1.5.6",
|
||||
"preact": "^8.2.5",
|
||||
"require-dir": "^0.3.2",
|
||||
"rollup": "^0.50.0",
|
||||
"rollup-plugin-babel": "^3.0.2",
|
||||
"rollup-plugin-commonjs": "^8.2.1",
|
||||
"rollup-plugin-node-resolve": "^3.0.0",
|
||||
"rollup-plugin-replace": "^2.0.0",
|
||||
"rollup-plugin-uglify": "^2.0.1",
|
||||
"rollup-watch": "^4.3.1",
|
||||
"run-sequence": "^2.2.0",
|
||||
"sw-precache": "^5.2.0",
|
||||
"uglify-js": "^3.1.3",
|
||||
"vulcanize": "^1.16.0"
|
||||
"mocha": "^5.2.0",
|
||||
"parse5": "^5.0.0",
|
||||
"polymer-analyzer": "^3.0.1",
|
||||
"polymer-build": "^3.0.2",
|
||||
"polymer-bundler": "^4.0.1",
|
||||
"polymer-cli": "^1.7.4",
|
||||
"pump": "^3.0.0",
|
||||
"reify": "^0.16.2",
|
||||
"require-dir": "^1.0.0",
|
||||
"sinon": "^6.0.0",
|
||||
"uglify-es": "^3.3.9",
|
||||
"uglify-js": "^3.4.1",
|
||||
"uglifyjs-webpack-plugin": "^1.2.6",
|
||||
"wct-browser-legacy": "^1.0.1",
|
||||
"web-component-tester": "^6.7.0",
|
||||
"webpack": "^4.12.0",
|
||||
"webpack-cli": "^3.0.8",
|
||||
"workbox-webpack-plugin": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"web-component-tester": "^6.3.0"
|
||||
}
|
||||
"resolutions": {
|
||||
"inherits": "2.0.3",
|
||||
"samsam": "1.1.3",
|
||||
"supports-color": "3.1.2",
|
||||
"type-detect": "1.0.0",
|
||||
"@webcomponents/webcomponentsjs": "2.0.2",
|
||||
"@webcomponents/shadycss": "^1.3.1",
|
||||
"@vaadin/vaadin-overlay": "3.0.2-pre.2",
|
||||
"fecha": "https://github.com/balloob/fecha/archive/51d14fd0eb4781e2ecf265d1c3080706259133b5.tar.gz"
|
||||
},
|
||||
"main": "src/home-assistant.js"
|
||||
}
|
||||
|
@@ -1,151 +0,0 @@
|
||||
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
|
||||
<link rel="import" href="../../../bower_components/app-layout/app-header-layout/app-header-layout.html">
|
||||
<link rel="import" href="../../../bower_components/app-layout/app-header/app-header.html">
|
||||
<link rel="import" href="../../../bower_components/app-layout/app-toolbar/app-toolbar.html">
|
||||
<link rel="import" href="../../../bower_components/paper-card/paper-card.html">
|
||||
<link rel="import" href="../../../bower_components/paper-item/paper-item.html">
|
||||
<link rel="import" href="../../../bower_components/paper-item/paper-item-body.html">
|
||||
<link rel="import" href="../../../bower_components/paper-fab/paper-fab.html">
|
||||
<link rel="import" href="../../../bower_components/paper-icon-button/paper-icon-button.html">
|
||||
|
||||
<link rel='import' href='../../../src/util/hass-mixins.html'>
|
||||
|
||||
<link rel="import" href="../ha-config-section.html">
|
||||
|
||||
<dom-module id="ha-automation-picker">
|
||||
<template>
|
||||
<style include="ha-style">
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* .content {
|
||||
padding: 16px;
|
||||
}
|
||||
*/
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
paper-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
paper-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<app-header-layout has-scrolling-region>
|
||||
<app-header slot="header" fixed>
|
||||
<app-toolbar>
|
||||
<paper-icon-button
|
||||
icon='mdi:arrow-left'
|
||||
on-tap='_backTapped'
|
||||
></paper-icon-button>
|
||||
<div main-title>Automations</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
|
||||
<ha-config-section
|
||||
is-wide='[[isWide]]'
|
||||
>
|
||||
<div slot='header'>Automation Editor</div>
|
||||
<div slot='introduction'>
|
||||
The automation editor allows you to create and edit automations.
|
||||
Please read <a href='https://home-assistant.io/docs/automation/editor/' target='_blank'>the instructions</a> to make sure that you have configured Home Assistant correctly.
|
||||
</div>
|
||||
|
||||
<paper-card heading='Pick automation to edit'>
|
||||
<template is='dom-if' if='[[!automations.length]]'>
|
||||
<div class='card-content'>
|
||||
<p>We couldn't find any editable automations.</p>
|
||||
</div>
|
||||
</template>
|
||||
<template is='dom-repeat' items='[[automations]]' as='automation'>
|
||||
<paper-item>
|
||||
<paper-item-body two-line on-tap='automationTapped'>
|
||||
<div>[[computeName(automation)]]</div>
|
||||
<div secondary>[[computeDescription(automation)]]</div>
|
||||
</paper-item-body>
|
||||
<iron-icon icon='mdi:chevron-right'></iron-icon>
|
||||
</paper-item>
|
||||
</template>
|
||||
</paper-card>
|
||||
</ha-config-section>
|
||||
|
||||
<paper-fab
|
||||
is-wide$='[[isWide]]'
|
||||
icon='mdi:plus'
|
||||
title='Add Automation'
|
||||
on-tap='addAutomation'
|
||||
></paper-fab>
|
||||
</app-header-layout>
|
||||
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
class HaAutomationPicker extends window.hassMixins.EventsMixin(Polymer.Element) {
|
||||
static get is() { return 'ha-automation-picker'; }
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
showMenu: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
automations: {
|
||||
type: Array,
|
||||
},
|
||||
|
||||
isWide: {
|
||||
type: Boolean,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
automationTapped(ev) {
|
||||
history.pushState(null, null, '/config/automation/edit/' + this.automations[ev.model.index].attributes.id);
|
||||
this.fire('location-changed');
|
||||
}
|
||||
|
||||
addAutomation() {
|
||||
history.pushState(null, null, '/config/automation/new');
|
||||
this.fire('location-changed');
|
||||
}
|
||||
|
||||
computeName(automation) {
|
||||
return window.hassUtil.computeStateName(automation);
|
||||
}
|
||||
|
||||
// Still thinking of something to add here.
|
||||
// eslint-disable-next-line
|
||||
computeDescription(automation) {
|
||||
return '';
|
||||
}
|
||||
|
||||
_backTapped() {
|
||||
history.back();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(HaAutomationPicker.is, HaAutomationPicker);
|
||||
</script>
|
@@ -1,82 +0,0 @@
|
||||
<link rel="import" href='../../../bower_components/polymer/polymer-element.html'>
|
||||
<link rel="import" href='../../../bower_components/paper-card/paper-card.html'>
|
||||
<link rel="import" href="../../../bower_components/paper-item/paper-item-body.html">
|
||||
<link rel="import" href='../../../bower_components/paper-button/paper-button.html'>
|
||||
|
||||
<link rel="import" href="../../../src/layouts/hass-subpage.html">
|
||||
<link rel="import" href="../../../src/util/hass-mixins.html">
|
||||
<link rel="import" href='../../../src/resources/ha-style.html'>
|
||||
|
||||
<dom-module id="ha-config-cloud-account">
|
||||
<template>
|
||||
<style include="iron-flex ha-style">
|
||||
.content {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
paper-card {
|
||||
display: block;
|
||||
}
|
||||
.account {
|
||||
display: flex;
|
||||
padding: 0 16px;
|
||||
}
|
||||
paper-button {
|
||||
align-self: center;
|
||||
}
|
||||
.soon {
|
||||
font-style: italic;
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
<hass-subpage title='Cloud Account'>
|
||||
<div class='content'>
|
||||
<ha-config-section
|
||||
is-wide='[[isWide]]'
|
||||
>
|
||||
<span slot='header'>Home Assistant Cloud</span>
|
||||
<span slot='introduction'>
|
||||
The Home Assistant Cloud allows you to opt-in to functions that will bring your Home Assistant experience to the next level.
|
||||
|
||||
<p><i>
|
||||
Home Assistant will never share information with our cloud without your prior permission.
|
||||
</i></p>
|
||||
</span>
|
||||
|
||||
<paper-card>
|
||||
<div class='account'>
|
||||
<paper-item-body>
|
||||
[[account.email]]
|
||||
</paper-item-body>
|
||||
<paper-button
|
||||
class='warning'
|
||||
on-tap='handleLogout'
|
||||
>Sign out</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
|
||||
<div class='soon'>More configuration options coming soon.</div>
|
||||
</ha-config-section>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
class HaConfigCloudAccount extends window.hassMixins.EventsMixin(Polymer.Element) {
|
||||
static get is() { return 'ha-config-cloud-account'; }
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
account: Object,
|
||||
};
|
||||
}
|
||||
|
||||
handleLogout() {
|
||||
this.hass.callApi('post', 'cloud/logout').then(() => this.fire('ha-account-refreshed', { account: null }));
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(HaConfigCloudAccount.is, HaConfigCloudAccount);
|
||||
</script>
|
@@ -1,233 +0,0 @@
|
||||
<link rel="import" href='../../../bower_components/polymer/polymer-element.html'>
|
||||
<link rel="import" href='../../../bower_components/paper-card/paper-card.html'>
|
||||
<link rel="import" href='../../../bower_components/paper-button/paper-button.html'>
|
||||
<link rel="import" href='../../../bower_components/paper-input/paper-input.html'>
|
||||
|
||||
<link rel="import" href="../../../src/layouts/hass-subpage.html">
|
||||
<link rel="import" href="../../../src/util/hass-mixins.html">
|
||||
<link rel="import" href='../../../src/resources/ha-style.html'>
|
||||
<link rel="import" href='../../../src/components/buttons/ha-progress-button.html'>
|
||||
|
||||
<dom-module id="ha-config-cloud-forgot-password">
|
||||
<template>
|
||||
<style include="iron-flex ha-style">
|
||||
.content {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
paper-card {
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
margin-top: 24px;
|
||||
}
|
||||
h1 {
|
||||
@apply(--paper-font-headline);
|
||||
margin: 0;
|
||||
}
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.card-actions a {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<hass-subpage title="Forgot Password">
|
||||
<div class='content'>
|
||||
<template is='dom-if' if='[[!_hasToken]]'>
|
||||
<paper-card>
|
||||
<div class='card-content'>
|
||||
<h1>Forgot Password</h1>
|
||||
<p>
|
||||
Enter your email address and we will send you a link to reset your password.
|
||||
</p>
|
||||
<paper-input
|
||||
autofocus
|
||||
label='E-mail'
|
||||
value='{{email}}'
|
||||
type='email'
|
||||
on-keydown='_keyDown'
|
||||
></paper-input>
|
||||
<div class='error' hidden$='[[!error]]'>[[error]]</div>
|
||||
</div>
|
||||
<div class='card-actions'>
|
||||
<ha-progress-button
|
||||
on-tap='_handleEmailPasswordReset'
|
||||
progress='[[_requestInProgress]]'
|
||||
>Send reset email</ha-progress-button>
|
||||
<button
|
||||
class='link'
|
||||
hidden='[[_requestInProgress]]'
|
||||
on-click='_handleHaveToken'
|
||||
>have a token?</button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
|
||||
<template is='dom-if' if='[[_hasToken]]'>
|
||||
<paper-card>
|
||||
<div class='card-content'>
|
||||
<h1>Confirm new password</h1>
|
||||
<template is='dom-if' if='[[_showEmailInputForConfirmation]]'>
|
||||
<paper-input
|
||||
label='E-mail'
|
||||
type='email'
|
||||
value='{{email}}'
|
||||
on-keydown='_keyDown'
|
||||
></paper-input>
|
||||
</template>
|
||||
<paper-input
|
||||
label='Confirmation code'
|
||||
value='{{_confirmationCode}}'
|
||||
on-keydown='_keyDown'
|
||||
type='number'
|
||||
></paper-input>
|
||||
<paper-input
|
||||
label='New password'
|
||||
value='{{_newPassword}}'
|
||||
on-keydown='_keyDown'
|
||||
type='password'
|
||||
></paper-input>
|
||||
<div class='error' hidden$='[[!error]]'>[[error]]</div>
|
||||
</div>
|
||||
<div class='card-actions'>
|
||||
<ha-progress-button
|
||||
on-tap='_handleConfirmPasswordReset'
|
||||
progress='[[_requestInProgress]]'
|
||||
>Reset Password</ha-progress-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
class HaConfigCloudForgotPassword extends
|
||||
window.hassMixins.NavigateMixin(window.hassMixins.EventsMixin(Polymer.Element)) {
|
||||
static get is() { return 'ha-config-cloud-forgot-password'; }
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
email: {
|
||||
type: String,
|
||||
notify: true,
|
||||
},
|
||||
_hasToken: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
_newPassword: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
_confirmationCode: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
_showEmailInputForConfirmation: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
_requestInProgress: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return [
|
||||
'_inputChanged(email, _newPassword)',
|
||||
];
|
||||
}
|
||||
|
||||
_inputChanged() {
|
||||
this.error = false;
|
||||
}
|
||||
|
||||
_keyDown(ev) {
|
||||
// validate on enter
|
||||
if (ev.keyCode === 13) {
|
||||
if (this._hasToken) {
|
||||
this._handleConfirmPasswordReset();
|
||||
} else {
|
||||
this._handleEmailPasswordReset();
|
||||
}
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
_handleEmailPasswordReset() {
|
||||
if (!this.email) {
|
||||
this.error = 'Email is required.';
|
||||
}
|
||||
|
||||
if (this.error) return;
|
||||
|
||||
this._requestInProgress = true;
|
||||
|
||||
this.hass.callApi('post', 'cloud/forgot_password', {
|
||||
email: this.email,
|
||||
}).then(() => {
|
||||
this._hasToken = true;
|
||||
this._requestInProgress = false;
|
||||
}, (err) => {
|
||||
this._requestInProgress = false;
|
||||
this.error = err && err.body && err.body.message ?
|
||||
err.body.message : 'Unknown error';
|
||||
});
|
||||
}
|
||||
|
||||
_handleHaveToken() {
|
||||
this._error = '';
|
||||
this._showEmailInputForConfirmation = true;
|
||||
this._hasToken = true;
|
||||
}
|
||||
|
||||
_handleConfirmPasswordReset() {
|
||||
this.error = '';
|
||||
if (!this.email) {
|
||||
this.error += 'Email is required. ';
|
||||
}
|
||||
if (!this._confirmationCode) {
|
||||
this.error += 'Confirmation code is required. ';
|
||||
}
|
||||
if (!this._newPassword) {
|
||||
this.error += 'New password is required. ';
|
||||
} else if (this._newPassword.length < 6) {
|
||||
this.error += 'New password should be at least 6 characters.';
|
||||
}
|
||||
|
||||
if (this.error) return;
|
||||
|
||||
this._requestInProgress = true;
|
||||
|
||||
this.hass.callApi('post', 'cloud/confirm_forgot_password', {
|
||||
email: this.email,
|
||||
confirmation_code: this._confirmationCode,
|
||||
new_password: this._newPassword,
|
||||
}).then(() => {
|
||||
// eslint-disable-next-line
|
||||
alert('Password reset successful! You can now login.');
|
||||
this.navigate('config/cloud/login');
|
||||
}, (err) => {
|
||||
this._requestInProgress = false;
|
||||
this.error = err && err.body && err.body.message ?
|
||||
err.body.message : 'Unknown error';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(HaConfigCloudForgotPassword.is, HaConfigCloudForgotPassword);
|
||||
</script>
|
@@ -1,190 +0,0 @@
|
||||
<link rel="import" href='../../../bower_components/polymer/polymer-element.html'>
|
||||
<link rel="import" href='../../../bower_components/paper-card/paper-card.html'>
|
||||
<link rel="import" href='../../../bower_components/paper-button/paper-button.html'>
|
||||
<link rel="import" href='../../../bower_components/paper-input/paper-input.html'>
|
||||
|
||||
<link rel="import" href="../../../src/layouts/hass-subpage.html">
|
||||
<link rel="import" href="../../../src/util/hass-mixins.html">
|
||||
<link rel="import" href='../../../src/resources/ha-style.html'>
|
||||
<link rel="import" href='../../../src/components/buttons/ha-progress-button.html'>
|
||||
|
||||
<dom-module id="ha-config-cloud-login">
|
||||
<template>
|
||||
<style include="iron-flex ha-style">
|
||||
.content {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
paper-card {
|
||||
display: block;
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
paper-card:last-child {
|
||||
margin-top: 24px;
|
||||
}
|
||||
h1 {
|
||||
@apply(--paper-font-headline);
|
||||
margin: 0;
|
||||
}
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<hass-subpage title='Cloud Login'>
|
||||
<div class='content'>
|
||||
<ha-config-section
|
||||
is-wide='[[isWide]]'
|
||||
>
|
||||
<span slot='header'>Home Assistant Cloud</span>
|
||||
<span slot='introduction'>
|
||||
The Home Assistant Cloud allows you to opt-in to functions that will bring your Home Assistant experience to the next level.
|
||||
|
||||
<p><i>
|
||||
Home Assistant will never share information with our cloud without your prior permission.
|
||||
</i></p>
|
||||
</span>
|
||||
|
||||
<paper-card>
|
||||
<div class='card-content'>
|
||||
<h1>Sign In</h1>
|
||||
<paper-input
|
||||
label='Email'
|
||||
id='emailInput'
|
||||
type='email'
|
||||
value='{{email}}'
|
||||
on-keydown='_keyDown'
|
||||
></paper-input>
|
||||
<paper-input
|
||||
label='Password'
|
||||
value='{{_password}}'
|
||||
type='password'
|
||||
on-keydown='_keyDown'
|
||||
></paper-input>
|
||||
<div class='error' hidden$='[[!error]]'>[[error]]</div>
|
||||
</div>
|
||||
<div class='card-actions'>
|
||||
<ha-progress-button
|
||||
on-tap='_handleLogin'
|
||||
progress='[[_requestInProgress]]'
|
||||
>Sign in</ha-progress-button>
|
||||
<button
|
||||
class='link'
|
||||
hidden='[[_requestInProgress]]'
|
||||
on-click='_handleForgotPassword'
|
||||
>forgot password?</button>
|
||||
</div>
|
||||
</paper-card>
|
||||
|
||||
<paper-card>
|
||||
<paper-item on-tap='_handleRegister'>
|
||||
<paper-item-body two-line>
|
||||
Create Account
|
||||
<div secondary>It is free and allows easy integration with voice assistants.</div>
|
||||
</paper-item-body>
|
||||
<iron-icon icon='mdi:chevron-right'></iron-icon>
|
||||
</paper-item>
|
||||
</paper-card>
|
||||
</ha-config-section>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
class HaConfigCloudLogin extends
|
||||
window.hassMixins.NavigateMixin(window.hassMixins.EventsMixin(Polymer.Element)) {
|
||||
static get is() { return 'ha-config-cloud-login'; }
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
isWide: Boolean,
|
||||
email: {
|
||||
type: String,
|
||||
notify: true,
|
||||
},
|
||||
_password: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
_requestInProgress: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return [
|
||||
'_inputChanged(email, _password)'
|
||||
];
|
||||
}
|
||||
|
||||
_inputChanged() {
|
||||
this.error = false;
|
||||
}
|
||||
|
||||
_keyDown(ev) {
|
||||
// validate on enter
|
||||
if (ev.keyCode === 13) {
|
||||
this._handleLogin();
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
_handleLogin() {
|
||||
if (!this.email) {
|
||||
this.error = 'Email is required.';
|
||||
} else if (!this._password) {
|
||||
this.error = 'Password is required.';
|
||||
}
|
||||
|
||||
if (this.error) return;
|
||||
|
||||
this._requestInProgress = true;
|
||||
|
||||
this.hass.callApi('post', 'cloud/login', {
|
||||
email: this.email,
|
||||
password: this._password,
|
||||
}).then((account) => {
|
||||
this.fire('ha-account-refreshed', { account: account });
|
||||
this.email = '';
|
||||
this._password = '';
|
||||
}, (err) => {
|
||||
this._password = '';
|
||||
this._requestInProgress = false;
|
||||
if (!err || !err.body || !err.body.message) {
|
||||
this.error = 'Unknown error';
|
||||
return;
|
||||
} else if (err.body.code === 'UserNotConfirmed') {
|
||||
alert('You need to confirm your email before logging in.');
|
||||
this.navigate('/config/cloud/register#confirm');
|
||||
return;
|
||||
} else if (err.body.code === 'PasswordChangeRequired') {
|
||||
alert('You need to change your password before logging in.');
|
||||
this.navigate('/config/cloud/forgot-password');
|
||||
}
|
||||
this.error = err.body.message;
|
||||
});
|
||||
}
|
||||
|
||||
_handleRegister() {
|
||||
this.navigate('/config/cloud/register');
|
||||
}
|
||||
|
||||
_handleForgotPassword() {
|
||||
this.navigate('/config/cloud/forgot-password');
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(HaConfigCloudLogin.is, HaConfigCloudLogin);
|
||||
</script>
|
@@ -1,249 +0,0 @@
|
||||
<link rel="import" href='../../../bower_components/polymer/polymer-element.html'>
|
||||
<link rel="import" href='../../../bower_components/paper-card/paper-card.html'>
|
||||
<link rel="import" href='../../../bower_components/paper-button/paper-button.html'>
|
||||
<link rel="import" href='../../../bower_components/paper-input/paper-input.html'>
|
||||
|
||||
<link rel="import" href="../../../src/layouts/hass-subpage.html">
|
||||
<link rel="import" href="../../../src/util/hass-mixins.html">
|
||||
<link rel="import" href='../../../src/resources/ha-style.html'>
|
||||
<link rel="import" href='../../../src/components/buttons/ha-progress-button.html'>
|
||||
|
||||
<dom-module id="ha-config-cloud-register">
|
||||
<template>
|
||||
<style include="iron-flex ha-style">
|
||||
paper-card {
|
||||
display: block;
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
paper-card:last-child {
|
||||
margin-top: 24px;
|
||||
}
|
||||
h1 {
|
||||
@apply(--paper-font-headline);
|
||||
margin: 0;
|
||||
}
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<hass-subpage title="Register Account">
|
||||
<div class='content'>
|
||||
<ha-config-section
|
||||
is-wide='[[isWide]]'
|
||||
>
|
||||
<span slot='header'>Register with the Home Assistant Cloud</span>
|
||||
<span slot='introduction'>
|
||||
Register today to easily connect your Home Assistant to cloud-only services.
|
||||
|
||||
<p>
|
||||
By registering an account you agree to the following terms and conditions.
|
||||
<ul>
|
||||
<li><a href='https://home-assistant.io/tos/' target='_blank'>Terms and Conditions</a></li>
|
||||
<li><a href='https://home-assistant.io/privacy/' target='_blank'>Privacy Policy</a></li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p><i>
|
||||
Home Assistant will never share information with our cloud without your prior permission.
|
||||
</i></p>
|
||||
</span>
|
||||
|
||||
<template is='dom-if' if='[[!_hasConfirmationCode]]'>
|
||||
<paper-card>
|
||||
<div class='card-content'>
|
||||
<div class='header'>
|
||||
<h1>Register</h1>
|
||||
<div class='error' hidden$='[[!_error]]'>[[_error]]</div>
|
||||
</div>
|
||||
<paper-input
|
||||
autofocus
|
||||
label='Email address'
|
||||
type='email'
|
||||
value='{{email}}'
|
||||
on-keydown='_keyDown'
|
||||
></paper-input>
|
||||
<paper-input
|
||||
label='Password'
|
||||
value='{{_password}}'
|
||||
type='password'
|
||||
on-keydown='_keyDown'
|
||||
></paper-input>
|
||||
</div>
|
||||
<div class='card-actions'>
|
||||
<ha-progress-button
|
||||
on-tap='_handleRegister'
|
||||
progress='[[_requestInProgress]]'
|
||||
>Create Account</ha-progress-button>
|
||||
<button
|
||||
class='link'
|
||||
hidden='[[_requestInProgress]]'
|
||||
on-click='_handleShowVerifyAccount'
|
||||
>have confirmation code?</button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
|
||||
<template is='dom-if' if='[[_hasConfirmationCode]]'>
|
||||
<paper-card>
|
||||
<div class='card-content'>
|
||||
<div class='header'>
|
||||
<h1>Verify email</h1>
|
||||
<div class='error' hidden$='[[!_error]]'>[[_error]]</div>
|
||||
</div>
|
||||
<p>
|
||||
Check your email address, we've emailed you a verification code to activate your account.
|
||||
</p>
|
||||
<template is='dom-if' if='[[_showEmailInputForConfirmation]]'>
|
||||
<paper-input
|
||||
label='Email address'
|
||||
type='email'
|
||||
value='{{email}}'
|
||||
on-keydown='_keyDown'
|
||||
></paper-input>
|
||||
</template>
|
||||
<paper-input
|
||||
label='Confirmation code'
|
||||
value='{{_confirmationCode}}'
|
||||
on-keydown='_keyDown'
|
||||
type='number'
|
||||
></paper-input>
|
||||
</div>
|
||||
<div class='card-actions'>
|
||||
<ha-progress-button
|
||||
on-tap='_handleVerifyEmail'
|
||||
progress='[[_requestInProgress]]'
|
||||
>Verify Email</ha-progress-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
</ha-config-section>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
class HaConfigCloudRegister extends
|
||||
window.hassMixins.NavigateMixin(window.hassMixins.EventsMixin(Polymer.Element)) {
|
||||
static get is() { return 'ha-config-cloud-register'; }
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
isWide: Boolean,
|
||||
email: {
|
||||
type: String,
|
||||
notify: true,
|
||||
},
|
||||
|
||||
_requestInProgress: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
_password: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
_showEmailInputForConfirmation: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
_hasConfirmationCode: {
|
||||
type: Boolean,
|
||||
value: () => document.location.hash === '#confirm'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return [
|
||||
'_inputChanged(email, _password)'
|
||||
];
|
||||
}
|
||||
|
||||
_inputChanged() {
|
||||
this._error = false;
|
||||
}
|
||||
|
||||
_keyDown(ev) {
|
||||
// validate on enter
|
||||
if (ev.keyCode === 13) {
|
||||
if (this._hasConfirmationCode) {
|
||||
this._handleVerifyEmail();
|
||||
} else {
|
||||
this._handleRegister();
|
||||
}
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
_handleRegister() {
|
||||
if (!this.email) {
|
||||
this._error = 'Email is required.';
|
||||
} else if (!this._password) {
|
||||
this._error = 'Password is required.';
|
||||
}
|
||||
|
||||
if (this._error) return;
|
||||
|
||||
this._requestInProgress = true;
|
||||
|
||||
this.hass.callApi('post', 'cloud/register', {
|
||||
email: this.email,
|
||||
password: this._password,
|
||||
}).then(() => {
|
||||
this._requestInProgress = false;
|
||||
this._hasConfirmationCode = true;
|
||||
}, (err) => {
|
||||
this._password = '';
|
||||
this._requestInProgress = false;
|
||||
this._error = err && err.body && err.body.message ?
|
||||
err.body.message : 'Unknown error';
|
||||
});
|
||||
}
|
||||
|
||||
_handleShowVerifyAccount() {
|
||||
this._error = '';
|
||||
this._showEmailInputForConfirmation = true;
|
||||
this._hasConfirmationCode = true;
|
||||
}
|
||||
|
||||
_handleVerifyEmail() {
|
||||
if (!this.email) {
|
||||
this._error = 'Email is required.';
|
||||
} else if (!this._confirmationCode) {
|
||||
this._error = 'Confirmation code is required.';
|
||||
}
|
||||
|
||||
if (this._error) return;
|
||||
|
||||
this._requestInProgress = true;
|
||||
|
||||
this.hass.callApi('post', 'cloud/confirm_register', {
|
||||
email: this.email,
|
||||
confirmation_code: this._confirmationCode,
|
||||
}).then(() => {
|
||||
// eslint-disable-next-line
|
||||
alert('Confirmation successful. You can now login.');
|
||||
this.navigate('config/cloud/login');
|
||||
}, (err) => {
|
||||
this._confirmationCode = '';
|
||||
this._error = err && err.body && err.body.message ?
|
||||
err.body.message : 'Unknown error';
|
||||
this._requestInProgress = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(HaConfigCloudRegister.is, HaConfigCloudRegister);
|
||||
</script>
|
@@ -1,113 +0,0 @@
|
||||
<link rel="import" href='../../../bower_components/polymer/polymer-element.html'>
|
||||
<link rel='import' href='../../../bower_components/app-route/app-route.html'>
|
||||
|
||||
<link rel='import' href='../../../src/util/hass-mixins.html'>
|
||||
<link rel="import" href="../ha-config-section.html">
|
||||
|
||||
<link rel="import" href="./ha-config-cloud-login.html">
|
||||
<link rel="import" href="./ha-config-cloud-register.html">
|
||||
<link rel="import" href="./ha-config-cloud-forgot-password.html">
|
||||
<link rel="import" href="./ha-config-cloud-account.html">
|
||||
|
||||
<dom-module id="ha-config-cloud">
|
||||
<template>
|
||||
<style>
|
||||
iron-pages {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<app-route
|
||||
route='[[route]]'
|
||||
pattern='/:page'
|
||||
data="{{_routeData}}"
|
||||
tail="{{_routeTail}}"
|
||||
></app-route>
|
||||
|
||||
<template is='dom-if' if='[[account]]' restamp>
|
||||
<ha-config-cloud-account
|
||||
hass='[[hass]]'
|
||||
account='[[account]]'
|
||||
is-wide='[[isWide]]'
|
||||
></ha-config-cloud-account>
|
||||
</template>
|
||||
|
||||
<template is='dom-if' if='[[!account]]' restamp>
|
||||
<template is='dom-if' if='[[_isLoginPage(_routeData.page)]]' restamp>
|
||||
<ha-config-cloud-login
|
||||
page-name='login'
|
||||
hass='[[hass]]'
|
||||
is-wide='[[isWide]]'
|
||||
email='{{_loginEmail}}'
|
||||
></ha-config-cloud-login>
|
||||
</template>
|
||||
|
||||
<template is='dom-if' if='[[_isRegisterPage(_routeData.page)]]' restamp>
|
||||
<ha-config-cloud-register
|
||||
page-name='register'
|
||||
hass='[[hass]]'
|
||||
is-wide='[[isWide]]'
|
||||
email='{{_loginEmail}}'
|
||||
></ha-config-cloud-register>
|
||||
</template>
|
||||
|
||||
<template is='dom-if' if='[[_isForgotPasswordPage(_routeData.page)]]' restamp>
|
||||
<ha-config-cloud-forgot-password
|
||||
page-name='forgot-password'
|
||||
hass='[[hass]]'
|
||||
is-wide='[[isWide]]'
|
||||
email='{{_loginEmail}}'
|
||||
></ha-config-cloud-forgot-password>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
class HaConfigCloud extends window.hassMixins.NavigateMixin(Polymer.Element) {
|
||||
static get is() { return 'ha-config-cloud'; }
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
isWide: Boolean,
|
||||
loadingAccount: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
account: {
|
||||
type: Object,
|
||||
value: null,
|
||||
},
|
||||
|
||||
route: Object,
|
||||
|
||||
_routeData: Object,
|
||||
_routeTail: Object,
|
||||
_loginEmail: String,
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return [
|
||||
'_checkRoute(route, account)'
|
||||
];
|
||||
}
|
||||
|
||||
_checkRoute(route, account) {
|
||||
if (!route || route.prefix !== '/config/cloud') return;
|
||||
|
||||
if (!account && ['/forgot-password', '/register'].indexOf(route.path) === -1) {
|
||||
this.navigate('/config/cloud/login', true);
|
||||
} else if (account &&
|
||||
['/login', '/register', '/forgot-password'].indexOf(route.path) !== -1) {
|
||||
this.navigate('/config/cloud/account', true);
|
||||
}
|
||||
}
|
||||
|
||||
_isRegisterPage(page) { return page === 'register'; }
|
||||
_isForgotPasswordPage(page) { return page === 'forgot-password'; }
|
||||
_isLoginPage(page) { return page === 'login'; }
|
||||
}
|
||||
|
||||
customElements.define(HaConfigCloud.is, HaConfigCloud);
|
||||
</script>
|
@@ -1,124 +0,0 @@
|
||||
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
|
||||
<link rel="import" href="../../../bower_components/paper-icon-button/paper-icon-button.html">
|
||||
|
||||
<link rel="import" href="../../../src/resources/ha-style.html">
|
||||
<link rel="import" href="../../../src/resources/panel-imports.html">
|
||||
|
||||
<link rel="import" href="./ha-config-section-core.html">
|
||||
<!-- <link rel="import" href="./ha-config-section-group.html"> -->
|
||||
<link rel="import" href="./ha-config-section-hassbian.html">
|
||||
<link rel="import" href="./ha-config-section-translation.html">
|
||||
<link rel="import" href="./ha-config-section-themes.html">
|
||||
|
||||
<dom-module id="ha-config-core">
|
||||
<template>
|
||||
<style include="iron-flex ha-style">
|
||||
.content {
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.border {
|
||||
margin: 32px auto 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
max-width: 1040px;
|
||||
}
|
||||
|
||||
.narrow .border {
|
||||
max-width: 640px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<app-header-layout has-scrolling-region>
|
||||
<app-header slot="header" fixed>
|
||||
<app-toolbar>
|
||||
<paper-icon-button
|
||||
icon='mdi:arrow-left'
|
||||
on-tap='_backTapped'
|
||||
></paper-icon-button>
|
||||
<div main-title>General</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
|
||||
<div class$='[[computeClasses(isWide)]]'>
|
||||
<!--
|
||||
Sortable.js doesn't work in Polymer 2 making this panel useless.
|
||||
Disabling for now.
|
||||
<ha-config-section-group
|
||||
is-wide='[[isWide]]'
|
||||
hass='[[hass]]'
|
||||
></ha-config-section-group>
|
||||
-->
|
||||
|
||||
<ha-config-section-core
|
||||
is-wide='[[isWide]]'
|
||||
hass='[[hass]]'
|
||||
></ha-config-section-core>
|
||||
|
||||
<template is='dom-if' if='[[computeIsTranslationLoaded(hass)]]'>
|
||||
<div class='border'></div>
|
||||
<ha-config-section-translation
|
||||
is-wide='[[isWide]]'
|
||||
hass='[[hass]]'
|
||||
></ha-config-section-translation>
|
||||
</template>
|
||||
|
||||
<template is='dom-if' if='[[computeIsThemesLoaded(hass)]]'>
|
||||
<div class='border'></div>
|
||||
<ha-config-section-themes
|
||||
is-wide='[[isWide]]'
|
||||
hass='[[hass]]'
|
||||
></ha-config-section-themes>
|
||||
</template>
|
||||
|
||||
<template is='dom-if' if='[[computeIsHassbianLoaded(hass)]]'>
|
||||
<div class='border'></div>
|
||||
<ha-config-section-hassbian
|
||||
is-wide='[[isWide]]'
|
||||
hass='[[hass]]'
|
||||
></ha-config-section-hassbian>
|
||||
</template>
|
||||
</div>
|
||||
</app-header-layout>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
class HaConfigCore extends Polymer.Element {
|
||||
static get is() { return 'ha-config-core'; }
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
isWide: Boolean,
|
||||
};
|
||||
}
|
||||
|
||||
computeClasses(isWide) {
|
||||
return isWide ? 'content' : 'content narrow';
|
||||
}
|
||||
|
||||
computeIsHassbianLoaded(hass) {
|
||||
return window.hassUtil.isComponentLoaded(hass, 'config.hassbian');
|
||||
}
|
||||
|
||||
computeIsZwaveLoaded(hass) {
|
||||
return window.hassUtil.isComponentLoaded(hass, 'config.zwave');
|
||||
}
|
||||
|
||||
computeIsTranslationLoaded(hass) {
|
||||
return hass.translationMetadata &&
|
||||
Object.keys(hass.translationMetadata).length;
|
||||
}
|
||||
|
||||
computeIsThemesLoaded(hass) {
|
||||
return hass.themes && hass.themes.themes &&
|
||||
Object.keys(hass.themes.themes).length;
|
||||
}
|
||||
|
||||
_backTapped() {
|
||||
history.back();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(HaConfigCore.is, HaConfigCore);
|
||||
</script>
|
@@ -1,197 +0,0 @@
|
||||
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
|
||||
<link rel="import" href="../../../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../../../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../../../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../../../bower_components/paper-card/paper-card.html">
|
||||
|
||||
<link rel="import" href="../../../src/components/buttons/ha-call-service-button.html">
|
||||
<link rel="import" href="../../../src/resources/ha-style.html">
|
||||
|
||||
<link rel="import" href="../ha-config-section.html">
|
||||
|
||||
<dom-module id="ha-config-section-core">
|
||||
<template>
|
||||
<style include="iron-flex ha-style">
|
||||
.validate-container {
|
||||
@apply(--layout-vertical);
|
||||
@apply(--layout-center-center);
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.validate-result {
|
||||
color: var(--google-green-500);
|
||||
font-weight: 500;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.config-invalid {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.config-invalid .text {
|
||||
color: var(--google-red-500);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.config-invalid paper-button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.validate-log {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
<ha-config-section
|
||||
is-wide='[[isWide]]'
|
||||
>
|
||||
<span slot='header'>Configuration and Server Control</span>
|
||||
<span slot='introduction'>
|
||||
Changing your configuration can be a tiresome process. We know. This section will try to make your life a little bit easier.
|
||||
</span>
|
||||
|
||||
<paper-card heading='Configuration Validation'>
|
||||
<div class='card-content'>
|
||||
Validate your configuration if you recently made some changes to your configuration and want to make sure that it is all valid.
|
||||
<template is='dom-if' if='[[!validateLog]]'>
|
||||
<div class='validate-container'>
|
||||
<template is='dom-if' if='[[!validating]]'>
|
||||
<div class='validate-result' id='result'>[[validateResult]]</div>
|
||||
<paper-button raised on-tap='validateConfig'>check config</paper-button>
|
||||
</template>
|
||||
<template is='dom-if' if='[[validating]]'>
|
||||
<paper-spinner active></paper-spinner>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template is='dom-if' if='[[validateLog]]'>
|
||||
<div class='config-invalid'>
|
||||
<span class='text'>Configuration invalid.</span>
|
||||
<paper-button raised on-tap='validateConfig'>check config</paper-button>
|
||||
</div>
|
||||
<div id='configLog' class='validate-log'>[[validateLog]]</div>
|
||||
</template>
|
||||
</div>
|
||||
</paper-card>
|
||||
|
||||
<paper-card heading='Configuration Reloading'>
|
||||
<div class='card-content'>
|
||||
Some parts of Home Assistant can reload without requiring a restart. Hitting reload will unload their current configuration and load the new one.
|
||||
</div>
|
||||
<div class='card-actions'>
|
||||
<ha-call-service-button
|
||||
hass='[[hass]]'
|
||||
domain='homeassistant'
|
||||
service='reload_core_config'
|
||||
>Reload Core</ha-call-service-button>
|
||||
<ha-call-service-button
|
||||
hass='[[hass]]'
|
||||
domain='group'
|
||||
service='reload'
|
||||
hidden$='[[!groupLoaded(hass)]]'
|
||||
>Reload Groups</ha-call-service-button>
|
||||
<ha-call-service-button
|
||||
hass='[[hass]]'
|
||||
domain='automation'
|
||||
service='reload'
|
||||
hidden$='[[!automationLoaded(hass)]]'
|
||||
>Reload Automation</ha-call-service-button>
|
||||
<ha-call-service-button
|
||||
hass='[[hass]]'
|
||||
domain='script'
|
||||
service='reload'
|
||||
hidden$='[[!scriptLoaded(hass)]]'
|
||||
>Reload Scripts</ha-call-service-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
|
||||
<paper-card heading='Server Management'>
|
||||
<div class='card-content'>
|
||||
Control your Home Assistant server… from Home Assistant.
|
||||
</div>
|
||||
<div class='card-actions warning'>
|
||||
<ha-call-service-button
|
||||
class='warning'
|
||||
hass='[[hass]]'
|
||||
domain='homeassistant'
|
||||
service='restart'
|
||||
>Restart</ha-call-service-button>
|
||||
<ha-call-service-button
|
||||
class='warning'
|
||||
hass='[[hass]]'
|
||||
domain='homeassistant'
|
||||
service='stop'
|
||||
>Stop</ha-call-service-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
|
||||
</ha-config-section>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
class HaConfigSectionCore extends Polymer.Element {
|
||||
static get is() { return 'ha-config-section-core'; }
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
isWide: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
validating: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
validateResult: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
|
||||
validateLog: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
groupLoaded(hass) {
|
||||
return window.hassUtil.isComponentLoaded(hass, 'group');
|
||||
}
|
||||
|
||||
automationLoaded(hass) {
|
||||
return window.hassUtil.isComponentLoaded(hass, 'automation');
|
||||
}
|
||||
|
||||
scriptLoaded(hass) {
|
||||
return window.hassUtil.isComponentLoaded(hass, 'script');
|
||||
}
|
||||
|
||||
validateConfig() {
|
||||
this.validating = true;
|
||||
this.validateLog = '';
|
||||
this.validateResult = '';
|
||||
|
||||
var el = this;
|
||||
|
||||
this.hass.callApi('POST', 'config/core/check_config')
|
||||
.then(function (result) {
|
||||
el.validating = false;
|
||||
var isValid = el.configValid = result.result === 'valid';
|
||||
|
||||
if (isValid) {
|
||||
el.validateResult = 'Valid!';
|
||||
} else {
|
||||
el.validateLog = result.errors;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(HaConfigSectionCore.is, HaConfigSectionCore);
|
||||
</script>
|
@@ -1,82 +0,0 @@
|
||||
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
|
||||
<link rel="import" href="../../../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../../../bower_components/paper-card/paper-card.html">
|
||||
<link rel="import" href="../../../bower_components/paper-checkbox/paper-checkbox.html">
|
||||
<link rel="import" href="../../../bower_components/paper-spinner/paper-spinner.html">
|
||||
<link rel="import" href="../../../bower_components/paper-dropdown-menu/paper-dropdown-menu.html">
|
||||
<link rel='import' href='../../../bower_components/paper-listbox/paper-listbox.html'>
|
||||
<link rel='import' href='../../../bower_components/paper-item/paper-item.html'>
|
||||
|
||||
<link rel="import" href="../../../src/resources/ha-style.html">
|
||||
<link rel="import" href="../../../src/util/hass-util.html">
|
||||
|
||||
<link rel="import" href="../ha-config-section.html">
|
||||
<link rel="import" href="../ha-entity-config.html">
|
||||
<link rel="import" href="./ha-form-group.html">
|
||||
|
||||
<dom-module id="ha-config-section-group">
|
||||
<template>
|
||||
<ha-config-section is-wide='[[isWide]]'>
|
||||
<span slot='header'>Groups & Views</span>
|
||||
<span slot='introduction'>
|
||||
Use groups to organize your entities and make Home Assistant really your own.
|
||||
<br><br>
|
||||
Got more groups than you can handle? Create views to manage your groups.
|
||||
</span>
|
||||
|
||||
<ha-entity-config
|
||||
hass='[[hass]]'
|
||||
label='Group'
|
||||
entities='[[entities]]'
|
||||
config='[[entityConfig]]'>
|
||||
</ha-entity-config>
|
||||
</ha-config-section>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
class HaConfigSectionGroup extends Polymer.Element {
|
||||
static get is() { return 'ha-config-section-group'; }
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
isWide: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
entities: {
|
||||
type: Array,
|
||||
computed: 'computeEntities(hass)',
|
||||
},
|
||||
|
||||
entityConfig: {
|
||||
type: Object,
|
||||
value: {
|
||||
component: 'ha-form-group',
|
||||
computeSelectCaption: function (stateObj) {
|
||||
return window.hassUtil.computeStateName(stateObj) +
|
||||
(stateObj.attributes.view ? ' (view)' : '');
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
computeEntities(hass) {
|
||||
return Object.keys(hass.states)
|
||||
.map(function (key) { return hass.states[key]; })
|
||||
.filter(function (entity) {
|
||||
return (window.hassUtil.computeDomain(entity) === 'group' &&
|
||||
!entity.attributes.auto);
|
||||
})
|
||||
.sort(window.hassUtil.sortByName);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(HaConfigSectionGroup.is, HaConfigSectionGroup);
|
||||
</script>
|
@@ -1,172 +0,0 @@
|
||||
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
|
||||
<link rel='import' href='../../../bower_components/iron-media-query/iron-media-query.html'>
|
||||
<link rel="import" href="../../../bower_components/paper-icon-button/paper-icon-button.html">
|
||||
<link rel="import" href="../../../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../../../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../../../bower_components/paper-card/paper-card.html">
|
||||
|
||||
<link rel="import" href="../../../bower_components/app-layout/app-header-layout/app-header-layout.html">
|
||||
<link rel="import" href="../../../bower_components/app-layout/app-header/app-header.html">
|
||||
<link rel="import" href="../../../bower_components/app-layout/app-toolbar/app-toolbar.html">
|
||||
|
||||
<link rel="import" href="../../../src/components/ha-menu-button.html">
|
||||
<link rel="import" href="../../../src/resources/ha-style.html">
|
||||
|
||||
<link rel="import" href="../ha-config-section.html">
|
||||
|
||||
<dom-module id="ha-config-section-hassbian">
|
||||
<template>
|
||||
<style include="iron-flex ha-style">
|
||||
.header {
|
||||
font-size: 16px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.header .status {
|
||||
font-size: 14px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.card-actions paper-button {
|
||||
color: var(--default-primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
<ha-config-section is-wide='[[isWide]]'>
|
||||
<span slot='header'>Bring Hassbian to the next level</span>
|
||||
<span slot='introduction'>
|
||||
Discover exciting add-ons to enhance your Home Assistant installation. Add an MQTT server or control a connected TV via HDMI-CEC.
|
||||
</span>
|
||||
|
||||
<template is='dom-if' if='[[suiteStatus]]'>
|
||||
<template is='dom-repeat' items='[[computeSuiteKeys(suiteStatus)]]' as='suiteKey'>
|
||||
<paper-card>
|
||||
<div class='card-content'>
|
||||
<div class='header'>
|
||||
[[computeTitle(suiteKey)]]
|
||||
<span class='status'>
|
||||
[[computeSuiteStatus(suiteStatus, suiteKey)]]
|
||||
</span>
|
||||
</div>
|
||||
[[computeSuiteDescription(suiteStatus, suiteKey)]]
|
||||
</div>
|
||||
<div class='card-actions'>
|
||||
<paper-button on-tap='suiteMoreInfoTapped'>LEARN MORE</paper-button>
|
||||
|
||||
<template is='dom-if' if='[[computeShowInstall(suiteStatus, suiteKey)]]'>
|
||||
<paper-button on-tap='suiteActionTapped'>INSTALL</paper-button>
|
||||
</template>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
</template>
|
||||
</ha-config-section>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
class HaConfigSectionHassbian extends Polymer.Element {
|
||||
static get is() { return 'ha-config-section-hassbian'; }
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
isWide: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
suiteStatus: {
|
||||
type: Object,
|
||||
value: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
updateStatus() {
|
||||
// TODO tapping install while something is installing triggers a second
|
||||
// update loop to start.
|
||||
this.hass.callApi('GET', 'config/hassbian/suites').then(function (suites) {
|
||||
this.suiteStatus = suites;
|
||||
|
||||
var isInstalling = false;
|
||||
var keys = Object.keys(suites);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (suites[keys[i]].state === 'installing') {
|
||||
isInstalling = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isInstalling) {
|
||||
setTimeout(() => this.updateStatus(), 5000);
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.updateStatus = this.updateStatus.bind(this);
|
||||
this.updateStatus();
|
||||
}
|
||||
|
||||
computeSuiteKeys(suiteStatus) {
|
||||
// Prioritize installing packages
|
||||
return Object.keys(suiteStatus).sort(function (keyA, keyB) {
|
||||
var installingA = suiteStatus[keyA].state === 'installing';
|
||||
var installingB = suiteStatus[keyB].state === 'installing';
|
||||
|
||||
if (installingA && installingB) {
|
||||
// do nothing
|
||||
} else if (installingA) {
|
||||
return -1;
|
||||
} else if (installingB) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (keyA < keyB) {
|
||||
return -1;
|
||||
}
|
||||
if (keyA > keyB) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
computeSuiteDescription(suiteStatus, suiteKey) {
|
||||
return suiteStatus[suiteKey].description;
|
||||
}
|
||||
|
||||
computeTitle(suiteKey) {
|
||||
return suiteKey.substr(0, 1).toUpperCase() + suiteKey.substr(1);
|
||||
}
|
||||
|
||||
computeSuiteStatus(suiteStatus, suiteKey) {
|
||||
var state = suiteStatus[suiteKey].state.replace(/_/, ' ');
|
||||
return state.substr(0, 1).toUpperCase() + state.substr(1);
|
||||
}
|
||||
|
||||
computeShowStatus(suiteStatus, suiteKey) {
|
||||
var state = suiteStatus[suiteKey].state;
|
||||
return state !== 'installing' && state !== 'not_installed';
|
||||
}
|
||||
|
||||
computeShowInstall(suiteStatus, suiteKey) {
|
||||
return suiteStatus[suiteKey].state === 'not_installed';
|
||||
}
|
||||
|
||||
suiteMoreInfoTapped() {
|
||||
// console.log('learn more', ev.model.item);
|
||||
}
|
||||
|
||||
suiteActionTapped() {
|
||||
// TODO install tapped suite
|
||||
this.hass.callApi('POST', 'config/hassbian/suites/openzwave/install').then(this.updateStatus);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(HaConfigSectionHassbian.is, HaConfigSectionHassbian);
|
||||
</script>
|
@@ -1,93 +0,0 @@
|
||||
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
|
||||
<link rel="import" href="../../../bower_components/paper-card/paper-card.html">
|
||||
<link rel="import" href="../../../bower_components/paper-dropdown-menu/paper-dropdown-menu.html">
|
||||
<link rel='import' href='../../../bower_components/paper-listbox/paper-listbox.html'>
|
||||
<link rel='import' href='../../../bower_components/paper-item/paper-item.html'>
|
||||
|
||||
<link rel='import' href='../../../src/util/hass-mixins.html'>
|
||||
<link rel="import" href="../ha-config-section.html">
|
||||
|
||||
<dom-module id="ha-config-section-themes">
|
||||
<template>
|
||||
<ha-config-section is-wide='[[isWide]]'>
|
||||
<span slot='header'>Set a theme</span>
|
||||
<span slot='introduction'>
|
||||
Choose 'Backend-selected' to use whatever theme the backend chooses or pick a theme for this device.
|
||||
</span>
|
||||
|
||||
<paper-card>
|
||||
<div class='card-content'>
|
||||
<paper-dropdown-menu label='Theme' dynamic-align>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
selected='{{selectedTheme}}'
|
||||
>
|
||||
<template is='dom-repeat' items='[[themes]]' as='theme'>
|
||||
<paper-item>[[theme]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
</div>
|
||||
</paper-card>
|
||||
</ha-config-section>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
class HaConfigSectionThemes extends window.hassMixins.EventsMixin(Polymer.Element) {
|
||||
static get is() { return 'ha-config-section-themes'; }
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
isWide: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
themes: {
|
||||
type: Array,
|
||||
computed: 'computeThemes(hass)',
|
||||
},
|
||||
|
||||
selectedTheme: {
|
||||
type: Number,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return [
|
||||
'selectionChanged(hass, selectedTheme)',
|
||||
];
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
if (this.hass.selectedTheme && this.themes.indexOf(this.hass.selectedTheme) > 0) {
|
||||
this.selectedTheme = this.themes.indexOf(this.hass.selectedTheme);
|
||||
} else if (!this.hass.selectedTheme) {
|
||||
this.selectedTheme = 0;
|
||||
}
|
||||
}
|
||||
|
||||
computeThemes(hass) {
|
||||
if (!hass) return [];
|
||||
return ['Backend-selected', 'default'].concat(Object.keys(hass.themes.themes).sort());
|
||||
}
|
||||
|
||||
selectionChanged(hass, selection) {
|
||||
if (selection > 0 && selection < this.themes.length) {
|
||||
if (hass.selectedTheme !== this.themes[selection]) {
|
||||
this.fire('settheme', this.themes[selection]);
|
||||
}
|
||||
} else if (selection === 0 && hass.selectedTheme !== '') {
|
||||
this.fire('settheme', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(HaConfigSectionThemes.is, HaConfigSectionThemes);
|
||||
</script>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user