Compare commits
1787 Commits
20180802.0
...
untagged-9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54beaad7e5 | ||
|
|
e30f9d4a66 | ||
|
|
27264b27a9 | ||
|
|
2b1f9460a8 | ||
|
|
1f6fe5dfcf | ||
|
|
058b4ba658 | ||
|
|
fad2f1790f | ||
|
|
8fd8274d15 | ||
|
|
f4f1e24ad5 | ||
|
|
42626ba2f8 | ||
|
|
29ab04fc7a | ||
|
|
722e9bcda7 | ||
|
|
065e42c8fd | ||
|
|
bf343647d4 | ||
|
|
3b51e55f2d | ||
|
|
125616aa99 | ||
|
|
1341fe9ae9 | ||
|
|
b195df0bfa | ||
|
|
1109d18576 | ||
|
|
b1a6580afb | ||
|
|
1d95b9d779 | ||
|
|
202782e741 | ||
|
|
a4663d438c | ||
|
|
eab3e6091a | ||
|
|
6627a96a05 | ||
|
|
16ae52c321 | ||
|
|
9792572370 | ||
|
|
4bb65b8ae1 | ||
|
|
2f3b399450 | ||
|
|
3e98b8e4f1 | ||
|
|
493198f530 | ||
|
|
a2c2f6a1e2 | ||
|
|
b46c9406ff | ||
|
|
9f213cf055 | ||
|
|
3254478d05 | ||
|
|
321b852079 | ||
|
|
8b44998e1f | ||
|
|
4aeca70f49 | ||
|
|
7912f0bf9e | ||
|
|
abc849f623 | ||
|
|
e6671299fe | ||
|
|
8c5beb0042 | ||
|
|
eba3c535bf | ||
|
|
34d50f0c90 | ||
|
|
9eae637814 | ||
|
|
a29d598027 | ||
|
|
5448cbf1c5 | ||
|
|
f2999c30f3 | ||
|
|
8a710202f1 | ||
|
|
11f917d5f8 | ||
|
|
2d8d6119bd | ||
|
|
1a5ae99c42 | ||
|
|
c4d888f060 | ||
|
|
594ee7ce9b | ||
|
|
7f10bcbfd1 | ||
|
|
fe31f532b6 | ||
|
|
7e7158b816 | ||
|
|
e19c210af2 | ||
|
|
a2f23c068b | ||
|
|
205e12150f | ||
|
|
b7ea66c30f | ||
|
|
11ac8e4b08 | ||
|
|
cdfc3f8faf | ||
|
|
44ca37c1dc | ||
|
|
535308bf96 | ||
|
|
6328f15032 | ||
|
|
2f96a096f7 | ||
|
|
cbd01f2d68 | ||
|
|
2ff4d0fa4b | ||
|
|
3aba2e3408 | ||
|
|
b473c9c2aa | ||
|
|
b97e24283c | ||
|
|
c8d3293ae9 | ||
|
|
8e0c39e451 | ||
|
|
46968bb565 | ||
|
|
011219b727 | ||
|
|
9205837b67 | ||
|
|
4eed3508ce | ||
|
|
460a56aa0a | ||
|
|
3927eb53ac | ||
|
|
2a596666c8 | ||
|
|
0008a100f4 | ||
|
|
164e433592 | ||
|
|
c4fca84ded | ||
|
|
48a010563e | ||
|
|
4378904243 | ||
|
|
ba66bf88d3 | ||
|
|
5282a6504a | ||
|
|
4f3abe1025 | ||
|
|
5bcba95c25 | ||
|
|
a9c9d4ca51 | ||
|
|
f00ad84c16 | ||
|
|
3b2e02562c | ||
|
|
7bc947ffb0 | ||
|
|
15564a1b26 | ||
|
|
753e069323 | ||
|
|
b37a0e2d43 | ||
|
|
87b35010e0 | ||
|
|
4e383e3e67 | ||
|
|
0e82178973 | ||
|
|
fe2046c6cd | ||
|
|
af0304bf78 | ||
|
|
fcd206e94b | ||
|
|
cf7a300614 | ||
|
|
a97ce49f0b | ||
|
|
5bfdc98217 | ||
|
|
1bc2e6fc17 | ||
|
|
6b5c9efb39 | ||
|
|
be0c035ba1 | ||
|
|
12173388a0 | ||
|
|
ba0d7cb156 | ||
|
|
c3e29e359a | ||
|
|
6259e45128 | ||
|
|
6998cce8eb | ||
|
|
f43abb5a9d | ||
|
|
a7fdbc069b | ||
|
|
15a88385c2 | ||
|
|
6ddf364093 | ||
|
|
fccb97ede8 | ||
|
|
cfc6bf4da9 | ||
|
|
02e250cd04 | ||
|
|
62ae7df097 | ||
|
|
0d43bef600 | ||
|
|
3709c13975 | ||
|
|
b624b363bd | ||
|
|
d841cc92ef | ||
|
|
cdcafe9e6f | ||
|
|
a66960fa00 | ||
|
|
38cc7b1090 | ||
|
|
ee0388708f | ||
|
|
0717a3dadd | ||
|
|
6b0b66af99 | ||
|
|
7e5f28b3cc | ||
|
|
ecfbfbf56b | ||
|
|
9b321124bb | ||
|
|
512b76f450 | ||
|
|
5de8c713c8 | ||
|
|
f64062d17b | ||
|
|
afd6fddad7 | ||
|
|
e8ad975212 | ||
|
|
5edee41c5b | ||
|
|
c04a091f59 | ||
|
|
3bbd45079c | ||
|
|
01da25d2d6 | ||
|
|
355e3d7911 | ||
|
|
6c109c15ef | ||
|
|
c542b242fe | ||
|
|
bcb26bd960 | ||
|
|
46f3a38b7c | ||
|
|
864175bde9 | ||
|
|
f458bdffe0 | ||
|
|
200e099035 | ||
|
|
07b8518162 | ||
|
|
b3525abf21 | ||
|
|
f7bb85d332 | ||
|
|
b8a18a27a4 | ||
|
|
88bea10b26 | ||
|
|
807dff99af | ||
|
|
9fa8544972 | ||
|
|
204bd803bf | ||
|
|
52712f65c2 | ||
|
|
8c3f8656fe | ||
|
|
1f3a5b1396 | ||
|
|
c15629b81b | ||
|
|
ef3892de92 | ||
|
|
47d6bb69b0 | ||
|
|
f10fab7e22 | ||
|
|
e2dfac48d0 | ||
|
|
53f5a29151 | ||
|
|
a042cd2d48 | ||
|
|
8533f9372f | ||
|
|
a4e96a4f3f | ||
|
|
fa40135a27 | ||
|
|
d85f9f9021 | ||
|
|
dc2ee2e63f | ||
|
|
f3729759b7 | ||
|
|
f108e279cd | ||
|
|
8dce24ddfc | ||
|
|
d2e780dda2 | ||
|
|
c382768008 | ||
|
|
f369045f35 | ||
|
|
17921c18b6 | ||
|
|
4799fdee9c | ||
|
|
2049687590 | ||
|
|
aca5ae9f67 | ||
|
|
98b882d599 | ||
|
|
5b02a43c3f | ||
|
|
2da844a1fb | ||
|
|
0544027c38 | ||
|
|
2389f92448 | ||
|
|
2fda2ee742 | ||
|
|
17a3affb6f | ||
|
|
f6be398fb9 | ||
|
|
87e24d658b | ||
|
|
abf70c3a3e | ||
|
|
b9afa69ee5 | ||
|
|
d9628fd9a2 | ||
|
|
7d90429fa9 | ||
|
|
fa6d0949a2 | ||
|
|
aab967798a | ||
|
|
c523bae2c8 | ||
|
|
b77372fc9a | ||
|
|
70b06861d1 | ||
|
|
b158f15d93 | ||
|
|
4edcd5f2ef | ||
|
|
689e37782e | ||
|
|
dcfed5d7e1 | ||
|
|
54ea6176aa | ||
|
|
a91bb3cdbb | ||
|
|
6abbe72e4d | ||
|
|
dae0ecce6a | ||
|
|
c3118eada9 | ||
|
|
0f6d0b164f | ||
|
|
87293e4b15 | ||
|
|
ff80eef25d | ||
|
|
0cd263c532 | ||
|
|
3c366b2b85 | ||
|
|
a59f0086b5 | ||
|
|
1f1a3acc03 | ||
|
|
d09cf9c8ab | ||
|
|
f32eb971a4 | ||
|
|
56c08a1d07 | ||
|
|
2fd75742f1 | ||
|
|
70b18344b6 | ||
|
|
5ec58a723e | ||
|
|
dcb975c8ce | ||
|
|
ea0a0f510d | ||
|
|
9476557aee | ||
|
|
4555bd4240 | ||
|
|
75c7445dd9 | ||
|
|
da741238d2 | ||
|
|
3d0c994b9a | ||
|
|
a7077dbcb4 | ||
|
|
a66013ecd7 | ||
|
|
8265a55838 | ||
|
|
95d6cbd130 | ||
|
|
1ee9811644 | ||
|
|
99c5f2a88a | ||
|
|
cdfd9cea5c | ||
|
|
4f2b82d787 | ||
|
|
3fd0ee9d75 | ||
|
|
c7f7e72340 | ||
|
|
1205322342 | ||
|
|
4cefb9715c | ||
|
|
35b38db57f | ||
|
|
4f72eb5416 | ||
|
|
f3d1a421f4 | ||
|
|
f3c24dc0b3 | ||
|
|
e4cbdc29a2 | ||
|
|
c985977efc | ||
|
|
8fb991c5ce | ||
|
|
210c63ad14 | ||
|
|
8167b05cad | ||
|
|
e5a916032a | ||
|
|
56745b3723 | ||
|
|
ddf2c6cc0f | ||
|
|
84df2bd531 | ||
|
|
42c3e3e46c | ||
|
|
5141e0e923 | ||
|
|
b87c94e395 | ||
|
|
55aa5a0d12 | ||
|
|
eaaeb10c6d | ||
|
|
567769be5a | ||
|
|
3ebb30bd48 | ||
|
|
09a19d2e7f | ||
|
|
fabc49d17e | ||
|
|
00e9155546 | ||
|
|
8238b700b0 | ||
|
|
5ff33224ed | ||
|
|
07dee9c5bb | ||
|
|
9eaeafdd6a | ||
|
|
beb1fe1e64 | ||
|
|
cdb2a1a424 | ||
|
|
8bbc442b7e | ||
|
|
7a12cbf96e | ||
|
|
e36454f08f | ||
|
|
3865c1943c | ||
|
|
e7e3edfd97 | ||
|
|
4bdc82f0ed | ||
|
|
8e3b41885d | ||
|
|
8f3d5fdb7d | ||
|
|
f258aa2818 | ||
|
|
b4dd971829 | ||
|
|
e99d6f8e6a | ||
|
|
cc969e547c | ||
|
|
0e1ae3926b | ||
|
|
60c2bcc483 | ||
|
|
5d8e34e8be | ||
|
|
14a430a059 | ||
|
|
4ae347949a | ||
|
|
cdd007cc54 | ||
|
|
2929db5ba4 | ||
|
|
628692b2e9 | ||
|
|
7cfdc24a8c | ||
|
|
1c69aa122b | ||
|
|
25afb73ed7 | ||
|
|
5b5384032d | ||
|
|
a9d221147f | ||
|
|
4fdbec93b3 | ||
|
|
0a8703ad0a | ||
|
|
ddc11c1b12 | ||
|
|
317f43277e | ||
|
|
bf90642c9b | ||
|
|
6f77992387 | ||
|
|
deaccd6cd4 | ||
|
|
d7371ace6a | ||
|
|
453b1000c1 | ||
|
|
8c1aff7505 | ||
|
|
adf002c154 | ||
|
|
ed7b81e7a4 | ||
|
|
c0f6ee6a32 | ||
|
|
9408df6099 | ||
|
|
157bfd6f80 | ||
|
|
99da7ebfe6 | ||
|
|
6911df9ac4 | ||
|
|
8daeaab40b | ||
|
|
45c3c78b31 | ||
|
|
a64a35b861 | ||
|
|
5a25627219 | ||
|
|
203b14613f | ||
|
|
0a7cb39500 | ||
|
|
42e75e7cdf | ||
|
|
58e6be12af | ||
|
|
618d25ce48 | ||
|
|
9974510067 | ||
|
|
2faa0c5979 | ||
|
|
1479647062 | ||
|
|
3becefaf8b | ||
|
|
e804e62e66 | ||
|
|
2c3cc1fbc7 | ||
|
|
58cc76ab5a | ||
|
|
5783cdb0d2 | ||
|
|
4f07caebc6 | ||
|
|
c4b75b4534 | ||
|
|
ae82eabaec | ||
|
|
f8d3e55fe0 | ||
|
|
1462db0a76 | ||
|
|
86b36fb76b | ||
|
|
c6194622b1 | ||
|
|
be5c3efb23 | ||
|
|
999c243c94 | ||
|
|
483f82e554 | ||
|
|
e91f4567c2 | ||
|
|
029467139d | ||
|
|
29649abe3d | ||
|
|
266c80320b | ||
|
|
ae51300446 | ||
|
|
cbdb222f72 | ||
|
|
98c419ff03 | ||
|
|
88b9348a81 | ||
|
|
3e8606781e | ||
|
|
875afbd7ae | ||
|
|
3139b914d7 | ||
|
|
212a44b6ae | ||
|
|
60551168a2 | ||
|
|
32d9a6884f | ||
|
|
7002ab27c0 | ||
|
|
15c101109e | ||
|
|
316fed953a | ||
|
|
93934449c0 | ||
|
|
894a25c98e | ||
|
|
90f0d9fa00 | ||
|
|
83889a8fd7 | ||
|
|
4cfc429e75 | ||
|
|
2df829b79d | ||
|
|
7baf6382ac | ||
|
|
7d1f689ed9 | ||
|
|
4f448553f6 | ||
|
|
dd56671974 | ||
|
|
d8e0fd0ba5 | ||
|
|
a9320d4baf | ||
|
|
42475becf1 | ||
|
|
c30aca8484 | ||
|
|
25bdf50737 | ||
|
|
4a60479b74 | ||
|
|
f6d651304c | ||
|
|
85990c20ed | ||
|
|
8ea98023a5 | ||
|
|
acceaea410 | ||
|
|
d609155022 | ||
|
|
1add5077af | ||
|
|
a9cac343b0 | ||
|
|
1b441a752e | ||
|
|
03fee95f68 | ||
|
|
7fa4b18843 | ||
|
|
7b0fb949fd | ||
|
|
df10cff842 | ||
|
|
8b93af1b56 | ||
|
|
a396a4e666 | ||
|
|
8f278ec4bc | ||
|
|
032ebce0bc | ||
|
|
bb60b42f98 | ||
|
|
21ed717287 | ||
|
|
2d056bad81 | ||
|
|
8297e9e215 | ||
|
|
4ccf450ad4 | ||
|
|
fc056869a7 | ||
|
|
0bd5ff34d4 | ||
|
|
ffd272d3fe | ||
|
|
1eee186e79 | ||
|
|
3a05b1124a | ||
|
|
d14c6125da | ||
|
|
3ee357178e | ||
|
|
8f6fdea4eb | ||
|
|
be6b25f5be | ||
|
|
be4dd5b20b | ||
|
|
fe4811b278 | ||
|
|
d376457cec | ||
|
|
35e82a8e26 | ||
|
|
03735f0539 | ||
|
|
4cc812c1bf | ||
|
|
bdacd05fab | ||
|
|
ab157fdbff | ||
|
|
d94223a61e | ||
|
|
ebe3198c27 | ||
|
|
2b2d2effd2 | ||
|
|
8092e24af8 | ||
|
|
f019bb095d | ||
|
|
1ad9d2e54c | ||
|
|
b2b18cb814 | ||
|
|
e595637a10 | ||
|
|
d10a0b3b6c | ||
|
|
c24f8a2115 | ||
|
|
7691e3f2c2 | ||
|
|
6bbe8ff39f | ||
|
|
a1e9b4938f | ||
|
|
c826596529 | ||
|
|
7f47079750 | ||
|
|
642ba1adc3 | ||
|
|
fe80c7fe0e | ||
|
|
9309c5a1b6 | ||
|
|
575eb22608 | ||
|
|
be0bef3f1b | ||
|
|
970286bbba | ||
|
|
087c3b9c0e | ||
|
|
b12d1b13ca | ||
|
|
d0410e0884 | ||
|
|
a1bf06ceb2 | ||
|
|
d99744e054 | ||
|
|
e02d11a51f | ||
|
|
1b50100b6c | ||
|
|
309fecc9f3 | ||
|
|
46f3add520 | ||
|
|
a89f0bd1cd | ||
|
|
8408b8d41f | ||
|
|
13761a20c5 | ||
|
|
03d17a9761 | ||
|
|
5501cccc67 | ||
|
|
9340d9068e | ||
|
|
bbdaa4b7c1 | ||
|
|
c87c782b2c | ||
|
|
f0b1cd9032 | ||
|
|
2ed532e055 | ||
|
|
f70dafa192 | ||
|
|
af6ade8eb6 | ||
|
|
d77ae840d8 | ||
|
|
968eae7727 | ||
|
|
97d8a68455 | ||
|
|
7827cec212 | ||
|
|
746ad588ef | ||
|
|
95e918b6ac | ||
|
|
1e82cc22e4 | ||
|
|
fb2e1e5ebb | ||
|
|
fe2ae965b3 | ||
|
|
8924a5f043 | ||
|
|
32e68c1a4b | ||
|
|
89a35a0062 | ||
|
|
484b1c8444 | ||
|
|
cd5e274ffa | ||
|
|
f466a53ed4 | ||
|
|
1d40d94774 | ||
|
|
82e8ca2754 | ||
|
|
8c904fb012 | ||
|
|
fa13b95498 | ||
|
|
289611363e | ||
|
|
cb7048db23 | ||
|
|
b9f86f735b | ||
|
|
0e044acaa9 | ||
|
|
1223766523 | ||
|
|
db65af9c22 | ||
|
|
fcdb1b48a2 | ||
|
|
8729410dce | ||
|
|
adb92e1708 | ||
|
|
81088e0d07 | ||
|
|
34129cc7cb | ||
|
|
530be9155b | ||
|
|
aa33b00a1f | ||
|
|
57b917f297 | ||
|
|
aad7dc5d7d | ||
|
|
6c41c7b1ab | ||
|
|
8b98f375c2 | ||
|
|
8a86dd8426 | ||
|
|
5b12ca94e9 | ||
|
|
652cd10483 | ||
|
|
ca0ded8587 | ||
|
|
f943393ade | ||
|
|
d8f21d99af | ||
|
|
73ef03e33f | ||
|
|
c34dde815c | ||
|
|
1e85880d7b | ||
|
|
57abd4ae07 | ||
|
|
2624c1544b | ||
|
|
1e72ffc0c2 | ||
|
|
8ca70ace4c | ||
|
|
d66cf3f787 | ||
|
|
44df0f698c | ||
|
|
981dd5df63 | ||
|
|
cd6250c495 | ||
|
|
2f36304f06 | ||
|
|
30471b7cfb | ||
|
|
ff2f573dd0 | ||
|
|
38ddbf45c2 | ||
|
|
d79bf5e07e | ||
|
|
d05b1ef9cc | ||
|
|
c260591d4d | ||
|
|
87a7e63e31 | ||
|
|
a5dd3755e1 | ||
|
|
ad40d9927b | ||
|
|
f4cfbc6678 | ||
|
|
b3c1bead39 | ||
|
|
d220e56239 | ||
|
|
f967b4940a | ||
|
|
f44d5dca1c | ||
|
|
a9ed4e7943 | ||
|
|
a404acbf44 | ||
|
|
eaa2ce1462 | ||
|
|
bdd8699709 | ||
|
|
9f0b20634a | ||
|
|
a70d9195db | ||
|
|
d86253d582 | ||
|
|
d5a313445f | ||
|
|
f979febb76 | ||
|
|
a1a2a78531 | ||
|
|
6ed2d288e6 | ||
|
|
5c8e5d3539 | ||
|
|
bbae3291e1 | ||
|
|
5dbd5c7395 | ||
|
|
038f7b43d5 | ||
|
|
671e564037 | ||
|
|
8298d810a8 | ||
|
|
7428479f6b | ||
|
|
6b85910cdb | ||
|
|
4d7bb0df7d | ||
|
|
26a39b1bb8 | ||
|
|
e23f046c4d | ||
|
|
fe73213643 | ||
|
|
cbe5355d38 | ||
|
|
81b232f01e | ||
|
|
3e6be45f1f | ||
|
|
d26ed6fdb6 | ||
|
|
eda168247c | ||
|
|
4d2390daf4 | ||
|
|
5b861bb4c6 | ||
|
|
be6d89bb7a | ||
|
|
1c17210948 | ||
|
|
5257715145 | ||
|
|
8df9ac9dfa | ||
|
|
559164e159 | ||
|
|
70072786a1 | ||
|
|
cda29fcd07 | ||
|
|
31e351c75c | ||
|
|
cadcd845cc | ||
|
|
b07f95f956 | ||
|
|
7f99f1d9be | ||
|
|
8c7cdda3d3 | ||
|
|
8c222bb467 | ||
|
|
4dfdebb00a | ||
|
|
3947adbab4 | ||
|
|
81eab0bf1b | ||
|
|
0c406335f5 | ||
|
|
109c40b2d3 | ||
|
|
a362b08113 | ||
|
|
438d155c45 | ||
|
|
75f5325048 | ||
|
|
8f5f14fada | ||
|
|
8e290be9e7 | ||
|
|
9f97b583a8 | ||
|
|
8993e39c38 | ||
|
|
dc61a62149 | ||
|
|
22fdac4189 | ||
|
|
c52f437ee6 | ||
|
|
549db23ff5 | ||
|
|
6775a094c9 | ||
|
|
74a255add1 | ||
|
|
a77c951d55 | ||
|
|
e3896c359a | ||
|
|
56e3514e40 | ||
|
|
f4319d9b13 | ||
|
|
c134464f6a | ||
|
|
7b821aa363 | ||
|
|
4e6d00cf5c | ||
|
|
22e5792a8f | ||
|
|
e3e0d4618e | ||
|
|
aa1ac8f339 | ||
|
|
40863db138 | ||
|
|
eac37af18c | ||
|
|
7f8f99a414 | ||
|
|
a743a2c46b | ||
|
|
adc63e1e5a | ||
|
|
1d24b83e5c | ||
|
|
b3f9432ae1 | ||
|
|
c95a44c570 | ||
|
|
5080f4c2db | ||
|
|
44eaa3abad | ||
|
|
9a4215b5d5 | ||
|
|
004892e11a | ||
|
|
669358bf1a | ||
|
|
435b7d9cee | ||
|
|
9a2207b5cb | ||
|
|
324f0bb8a2 | ||
|
|
3b8f8f8189 | ||
|
|
702c17d658 | ||
|
|
e2a9cf0d3c | ||
|
|
8aa501b7bd | ||
|
|
45189c9163 | ||
|
|
86940f4d42 | ||
|
|
812c1362a6 | ||
|
|
6bf9ea5699 | ||
|
|
20ee3452dc | ||
|
|
ef18f9eac9 | ||
|
|
47faf2768c | ||
|
|
a2bed3dd90 | ||
|
|
4fab0b9717 | ||
|
|
06b70e2653 | ||
|
|
48aa9a2ad7 | ||
|
|
93d971f72b | ||
|
|
7e69df44d7 | ||
|
|
c743a48cf9 | ||
|
|
b82a1c75c4 | ||
|
|
be9402bd05 | ||
|
|
ebae469e7d | ||
|
|
d0d293fe21 | ||
|
|
bd6d082555 | ||
|
|
39190dda20 | ||
|
|
89a8e3da36 | ||
|
|
49f90671fb | ||
|
|
c4ece5e451 | ||
|
|
799bd973ca | ||
|
|
03dffa9905 | ||
|
|
1d1c981601 | ||
|
|
40025d44c2 | ||
|
|
42117fcba0 | ||
|
|
dc16abd637 | ||
|
|
8c71746952 | ||
|
|
6e504020bf | ||
|
|
7caf37275d | ||
|
|
c3f094eb9e | ||
|
|
feb3be1d17 | ||
|
|
2fe0398f37 | ||
|
|
42c7879c4d | ||
|
|
2586590bd9 | ||
|
|
59ee160f96 | ||
|
|
d4bc4bf7bc | ||
|
|
e2a182acee | ||
|
|
88131ade23 | ||
|
|
fb16156f8d | ||
|
|
6ba77b4fa5 | ||
|
|
c55291dd18 | ||
|
|
27b61776e8 | ||
|
|
baa13a1b6c | ||
|
|
23ca1b972d | ||
|
|
2d75e797c7 | ||
|
|
117ea32586 | ||
|
|
68909c80ff | ||
|
|
7aa296e774 | ||
|
|
18df636573 | ||
|
|
e1540e45f9 | ||
|
|
d0b0284200 | ||
|
|
915c441a94 | ||
|
|
2aec877310 | ||
|
|
1e291e80b7 | ||
|
|
7d92eede1f | ||
|
|
9fc8c0764c | ||
|
|
4ff2d941c3 | ||
|
|
2349e2f251 | ||
|
|
8785b03fd8 | ||
|
|
92e6c5adfd | ||
|
|
6015eff8a2 | ||
|
|
1451a78dd3 | ||
|
|
094f558556 | ||
|
|
cd466df42c | ||
|
|
a626961ae5 | ||
|
|
bbc32278d8 | ||
|
|
e2ed1a9fd9 | ||
|
|
cd94442455 | ||
|
|
c9eea4acc1 | ||
|
|
e55ca54509 | ||
|
|
4118497978 | ||
|
|
882dc38b12 | ||
|
|
bcade77075 | ||
|
|
8c13e524b9 | ||
|
|
ffa47ccf34 | ||
|
|
1e22d13588 | ||
|
|
19804a713d | ||
|
|
eeaaecd5b7 | ||
|
|
9a00c65e3b | ||
|
|
c026c65d53 | ||
|
|
e9c245015c | ||
|
|
cdde6f6f4c | ||
|
|
262537c287 | ||
|
|
ec04c80413 | ||
|
|
86548052e5 | ||
|
|
1890dd8683 | ||
|
|
2908eb693a | ||
|
|
7fe4084073 | ||
|
|
ee948302ed | ||
|
|
f809bf0550 | ||
|
|
ed9dff99d3 | ||
|
|
f5d0162aec | ||
|
|
32682a2be0 | ||
|
|
836844a312 | ||
|
|
8b82fa940e | ||
|
|
d4be171df9 | ||
|
|
57be7ac873 | ||
|
|
a9cecb55ac | ||
|
|
1c6bf8b94a | ||
|
|
1c6235546a | ||
|
|
daaaef96b0 | ||
|
|
aa3b6343ed | ||
|
|
3e5d372bbe | ||
|
|
3bab8686c8 | ||
|
|
8c0af2c140 | ||
|
|
f008f8f41a | ||
|
|
587a7c3b66 | ||
|
|
b25fff9852 | ||
|
|
45e5f7d0ff | ||
|
|
19c44f7c7b | ||
|
|
241d7345d0 | ||
|
|
34c6356a47 | ||
|
|
9383d80354 | ||
|
|
178e4de452 | ||
|
|
787abc4611 | ||
|
|
c2948638d6 | ||
|
|
1db93a4f7b | ||
|
|
8f4d24b6da | ||
|
|
03d4a648f5 | ||
|
|
8dba463dd4 | ||
|
|
7c21a07a66 | ||
|
|
82189ab3c6 | ||
|
|
c0896d173d | ||
|
|
5032b6e63b | ||
|
|
9bf06ca0af | ||
|
|
4fa1c3e883 | ||
|
|
339be43eea | ||
|
|
00c08a09db | ||
|
|
bed257a4eb | ||
|
|
b73a2e838a | ||
|
|
6580d4ce92 | ||
|
|
8c23674683 | ||
|
|
3e28b6f2e2 | ||
|
|
6d2e480ed5 | ||
|
|
90a1f7e51c | ||
|
|
63e6506510 | ||
|
|
220fec6dc9 | ||
|
|
534b18ee30 | ||
|
|
e406a50b50 | ||
|
|
b764e87a00 | ||
|
|
b7f62c5822 | ||
|
|
dede819a12 | ||
|
|
083f0d16ef | ||
|
|
c7500a504d | ||
|
|
3348405518 | ||
|
|
70b2ff3365 | ||
|
|
a259a12eab | ||
|
|
979025539e | ||
|
|
6da311078a | ||
|
|
7d1991ac78 | ||
|
|
2c2199fb84 | ||
|
|
e12da05d4e | ||
|
|
25d10cf092 | ||
|
|
1cdaebd92f | ||
|
|
f5d3f1c042 | ||
|
|
4073238103 | ||
|
|
513eaea4f4 | ||
|
|
b4ac3ddfbd | ||
|
|
0a269c9e26 | ||
|
|
1f2371641e | ||
|
|
8b582f3fcf | ||
|
|
1afb8f109e | ||
|
|
5824e0b706 | ||
|
|
97deed9299 | ||
|
|
9efcca002d | ||
|
|
7904483272 | ||
|
|
8a9594d918 | ||
|
|
90c09e967a | ||
|
|
5ba1cc5075 | ||
|
|
12064a086b | ||
|
|
32d0e8bf1d | ||
|
|
79a5947587 | ||
|
|
c8cda3c817 | ||
|
|
197cf0f8cc | ||
|
|
392af26503 | ||
|
|
41343c9774 | ||
|
|
4afce7600b | ||
|
|
e4b4a94a5f | ||
|
|
3db79607b7 | ||
|
|
2ada32be02 | ||
|
|
a4ec8719f9 | ||
|
|
5e6b28d965 | ||
|
|
7d8f790708 | ||
|
|
b6b224be77 | ||
|
|
3b008b6359 | ||
|
|
da80bfa3c7 | ||
|
|
fcd06a9000 | ||
|
|
762908207f | ||
|
|
b40b5b95f1 | ||
|
|
fe176f2752 | ||
|
|
c7796e9557 | ||
|
|
679457e36a | ||
|
|
2d3d4db4dd | ||
|
|
f127bbc64d | ||
|
|
bdaf96b114 | ||
|
|
ad55bae212 | ||
|
|
ea8958adae | ||
|
|
e7b664a2ff | ||
|
|
541d1a5380 | ||
|
|
ac179f5b45 | ||
|
|
34f36c6179 | ||
|
|
56c1920cc1 | ||
|
|
456880c7cf | ||
|
|
08222dfbec | ||
|
|
e8d84e8ba5 | ||
|
|
c570ce9720 | ||
|
|
f8b66a78fa | ||
|
|
e2fc98526b | ||
|
|
856a393531 | ||
|
|
8bf2a2f8db | ||
|
|
3f6bbffcd6 | ||
|
|
4d5087bd8d | ||
|
|
f9663143a6 | ||
|
|
c4aac72e68 | ||
|
|
f4048bf4ba | ||
|
|
b384d17fd3 | ||
|
|
2ae30ac024 | ||
|
|
9b9b2f0710 | ||
|
|
5d58dfab3e | ||
|
|
38ba6058be | ||
|
|
0f779dd7f8 | ||
|
|
3ca842187a | ||
|
|
e36dada843 | ||
|
|
1b8c567fd7 | ||
|
|
e1c2cf770a | ||
|
|
ab6cd578e8 | ||
|
|
4058a0c8d0 | ||
|
|
421e5bb169 | ||
|
|
6faea73c9f | ||
|
|
3a644621fe | ||
|
|
abbfea0b6a | ||
|
|
2f2cdad16b | ||
|
|
aae3c26a64 | ||
|
|
0f680bcfd6 | ||
|
|
f71612d6cf | ||
|
|
5cc23ab084 | ||
|
|
9d6c0773c5 | ||
|
|
44dca3b86d | ||
|
|
310b81de04 | ||
|
|
f23258eb8c | ||
|
|
039bc587cc | ||
|
|
46e1139946 | ||
|
|
8938ad8f8d | ||
|
|
102cb06d28 | ||
|
|
17d0ae003a | ||
|
|
7a16961387 | ||
|
|
d3bdbce0d0 | ||
|
|
504e4987b7 | ||
|
|
f00de454d1 | ||
|
|
ce35416284 | ||
|
|
7773589e2c | ||
|
|
5d900f9ced | ||
|
|
7a344c865f | ||
|
|
bd0bc2047d | ||
|
|
2482d78a06 | ||
|
|
18fc0d0342 | ||
|
|
024ce5c379 | ||
|
|
f74fe5718e | ||
|
|
ef395d4c9f | ||
|
|
5d42e4f68d | ||
|
|
acce6f0b2f | ||
|
|
dadb5f92ee | ||
|
|
69aff1e204 | ||
|
|
810fd802b5 | ||
|
|
cf1b9e5067 | ||
|
|
83aaf4699c | ||
|
|
72aa98fe5c | ||
|
|
86b353e627 | ||
|
|
79183bb6ea | ||
|
|
4921686bdf | ||
|
|
a5bdf096dc | ||
|
|
bfee69e7ff | ||
|
|
db53d37493 | ||
|
|
c294124b8d | ||
|
|
2afc8607c6 | ||
|
|
e2ff51f425 | ||
|
|
25a579f7ed | ||
|
|
ecd33fd93c | ||
|
|
960707b804 | ||
|
|
4cd3b683a7 | ||
|
|
d0b507561d | ||
|
|
d8d3149558 | ||
|
|
41b086cd3c | ||
|
|
6fc20450b4 | ||
|
|
c00930f45e | ||
|
|
77935b7c7a | ||
|
|
03f7a23540 | ||
|
|
f1f1623d2f | ||
|
|
b86bfa0395 | ||
|
|
75235ec544 | ||
|
|
7cb2b743fa | ||
|
|
175693ba4e | ||
|
|
c7d1417f48 | ||
|
|
db92abad66 | ||
|
|
e42e59871e | ||
|
|
f97b5c48d0 | ||
|
|
f22510fd74 | ||
|
|
e789380126 | ||
|
|
9086051608 | ||
|
|
a0f0d49f45 | ||
|
|
bc87e05e2d | ||
|
|
b0958f589b | ||
|
|
b37eee56c0 | ||
|
|
2ad27f7400 | ||
|
|
95e9d6164d | ||
|
|
43bc9abb46 | ||
|
|
ab816ad529 | ||
|
|
c964ea30e0 | ||
|
|
73b500db64 | ||
|
|
633fc1372f | ||
|
|
dedb36cecf | ||
|
|
13aa0568a6 | ||
|
|
a090b291aa | ||
|
|
30ab056aa4 | ||
|
|
5a797a6dec | ||
|
|
d76ffd343e | ||
|
|
20ecfffb9c | ||
|
|
d2bfd5ce62 | ||
|
|
0cdb96f917 | ||
|
|
fd4ede39ba | ||
|
|
fa3889b549 | ||
|
|
d71d5aa855 | ||
|
|
644af4d009 | ||
|
|
9f4ae5d932 | ||
|
|
7a8c9d7c12 | ||
|
|
89630a5c7f | ||
|
|
13adee09da | ||
|
|
c3f473c3e7 | ||
|
|
0a09eabce3 | ||
|
|
3e1c22edcd | ||
|
|
ccb12996f8 | ||
|
|
d6887758a9 | ||
|
|
9299d548ba | ||
|
|
7dda98f139 | ||
|
|
6b8e90ce67 | ||
|
|
c20fae289c | ||
|
|
8db111c2fb | ||
|
|
5a6d537d43 | ||
|
|
1ffeace8f9 | ||
|
|
7bf8ea9d0a | ||
|
|
85e900bf68 | ||
|
|
6f9b7a1f60 | ||
|
|
cad4fa408e | ||
|
|
dc334844ed | ||
|
|
4e86cf01b8 | ||
|
|
2bfd7ff33b | ||
|
|
b1d5517864 | ||
|
|
334c4fe90f | ||
|
|
77f6016701 | ||
|
|
a2816800e8 | ||
|
|
e3d32c9dd0 | ||
|
|
37b2154638 | ||
|
|
28d3f445f6 | ||
|
|
45e50ea948 | ||
|
|
f3eaba4b23 | ||
|
|
96f7f842cd | ||
|
|
bc6debc6c9 | ||
|
|
a9df5ea6a9 | ||
|
|
c972d039bc | ||
|
|
d130471a99 | ||
|
|
52e31648bf | ||
|
|
40d12fa870 | ||
|
|
d5728579e0 | ||
|
|
257ff7707b | ||
|
|
b8851a7f3e | ||
|
|
a4680feb92 | ||
|
|
ca02080cf1 | ||
|
|
08824b5796 | ||
|
|
99ded45bb0 | ||
|
|
bc8cc32445 | ||
|
|
5cc75c294e | ||
|
|
035e6752eb | ||
|
|
c1913799f2 | ||
|
|
ceb86df0fa | ||
|
|
24b0eb8ce4 | ||
|
|
547f829f5b | ||
|
|
16c9303ae9 | ||
|
|
6d329bdd1f | ||
|
|
1c6f7d32cf | ||
|
|
9a86b06092 | ||
|
|
0e1eaa18df | ||
|
|
ad98534195 | ||
|
|
b261e779e5 | ||
|
|
f9e97c0577 | ||
|
|
5276db5d23 | ||
|
|
6bf954ccb2 | ||
|
|
26dbef8d4c | ||
|
|
1d1e85e1d7 | ||
|
|
755a3d5cf1 | ||
|
|
e9c5011a6d | ||
|
|
871ee33229 | ||
|
|
3b66d58f91 | ||
|
|
69c10256ef | ||
|
|
480b92c960 | ||
|
|
6098b433be | ||
|
|
d91bf4c2a9 | ||
|
|
bad9369ce6 | ||
|
|
3ba9189612 | ||
|
|
bb71fe0bec | ||
|
|
65359aabe3 | ||
|
|
233b646917 | ||
|
|
3263d84def | ||
|
|
faee6a7163 | ||
|
|
00ad91af9a | ||
|
|
4081fea5f2 | ||
|
|
97f7da198c | ||
|
|
f6be8153bb | ||
|
|
d67e035198 | ||
|
|
f7a67af71b | ||
|
|
41554ab14d | ||
|
|
1b2e052f05 | ||
|
|
427f0f5e66 | ||
|
|
86502c5cd8 | ||
|
|
edf7485578 | ||
|
|
01c5b64c63 | ||
|
|
690188b4a3 | ||
|
|
42e60beb0d | ||
|
|
0ddc82601a | ||
|
|
f943366ecd | ||
|
|
732237d4e1 | ||
|
|
d8d77d0238 | ||
|
|
f98fff9ffd | ||
|
|
9565b5490b | ||
|
|
8b19ea8e87 | ||
|
|
2e6d79a60f | ||
|
|
07f624fd1c | ||
|
|
3408392aea | ||
|
|
864f2619a2 | ||
|
|
bd2c57169b | ||
|
|
8f1c75c57b | ||
|
|
1c86fbd52c | ||
|
|
937a939907 | ||
|
|
7173b16ef3 | ||
|
|
d738f6e2f6 | ||
|
|
b0d32b5674 | ||
|
|
0f53b7c832 | ||
|
|
668d4e82ba | ||
|
|
963bdcc53c | ||
|
|
d34dada9d8 | ||
|
|
5ae599b1b2 | ||
|
|
ebbd308be6 | ||
|
|
6d43c9e86a | ||
|
|
339a0f70e3 | ||
|
|
94df67a7cb | ||
|
|
1d4f74cda3 | ||
|
|
07a4505f1e | ||
|
|
e96c9daad6 | ||
|
|
08f3496818 | ||
|
|
326931277e | ||
|
|
a2ef8bbe70 | ||
|
|
4bcb51bf5a | ||
|
|
bda5c3a0c9 | ||
|
|
e228f60c39 | ||
|
|
67c032c85a | ||
|
|
417ffde3e8 | ||
|
|
f3064f0071 | ||
|
|
e9d912cc87 | ||
|
|
2517e5ba60 | ||
|
|
64b405dd4d | ||
|
|
ddb050d1fd | ||
|
|
3f6a8cac80 | ||
|
|
ad113367e6 | ||
|
|
f4f08ab0d1 | ||
|
|
c2a57099d3 | ||
|
|
adf0c6d891 | ||
|
|
38a2627227 | ||
|
|
5a90edc893 | ||
|
|
88473581c2 | ||
|
|
88d23eb9dd | ||
|
|
25c788871f | ||
|
|
f272801253 | ||
|
|
2e750dc1e2 | ||
|
|
3c5fb6d1ad | ||
|
|
32cd683b8a | ||
|
|
6c029b39e0 | ||
|
|
7efad04e42 | ||
|
|
b6f7781a87 | ||
|
|
16a147f389 | ||
|
|
79b71ed753 | ||
|
|
49fa74cc07 | ||
|
|
0a2eaec884 | ||
|
|
4c5d3138c1 | ||
|
|
5e1cd389b3 | ||
|
|
7ced08a899 | ||
|
|
603cf7ba0f | ||
|
|
c47ba65c3b | ||
|
|
849ed80e78 | ||
|
|
b78c48ecec | ||
|
|
8d2da9c5a6 | ||
|
|
9664e8258c | ||
|
|
5f5bf17df0 | ||
|
|
ca7674cd15 | ||
|
|
3f5f5bb1ee | ||
|
|
e7ee9c7054 | ||
|
|
4f6ecf5c21 | ||
|
|
87eac4cdee | ||
|
|
d267196bff | ||
|
|
f683337cbe | ||
|
|
1a6226270f | ||
|
|
64714c64c7 | ||
|
|
b7c34c483a | ||
|
|
e5bf842801 | ||
|
|
d1a56d6acc | ||
|
|
cac7f8d1ab | ||
|
|
9d2b37c9f2 | ||
|
|
e20a02c52c | ||
|
|
c46d04eaa6 | ||
|
|
2ec8b97378 | ||
|
|
b3b9ca9c3f | ||
|
|
71ed83ef07 | ||
|
|
47635055d0 | ||
|
|
0dfca2f33b | ||
|
|
18de427145 | ||
|
|
118f28285e | ||
|
|
6a9cfbfa1c | ||
|
|
8c61624a9c | ||
|
|
d277571735 | ||
|
|
a6f3684846 | ||
|
|
edef4ba2f5 | ||
|
|
7cd6619a79 | ||
|
|
2059e36dd6 | ||
|
|
4a455e9147 | ||
|
|
fe0b131480 | ||
|
|
b1b78c2bb7 | ||
|
|
99395360c7 | ||
|
|
bd46e3b8e0 | ||
|
|
80dd15306e | ||
|
|
88f0ebf75d | ||
|
|
8679f10f10 | ||
|
|
db4c1e45f5 | ||
|
|
65cf2feb7a | ||
|
|
97da26eba7 | ||
|
|
8e7d7c5188 | ||
|
|
767307ef47 | ||
|
|
ccc6262026 | ||
|
|
2cdb542112 | ||
|
|
4e232e58ce | ||
|
|
27bb175624 | ||
|
|
5a5a7dad1e | ||
|
|
2d1cf421ef | ||
|
|
8be25f2020 | ||
|
|
0a8f853a8e | ||
|
|
a46f5e3d4e | ||
|
|
5de36f9579 | ||
|
|
9b5e79f42a | ||
|
|
a824599a37 | ||
|
|
884b24da0e | ||
|
|
76325a384c | ||
|
|
e2218f1e6e | ||
|
|
758b686684 | ||
|
|
3a50d47dd2 | ||
|
|
b4d4591273 | ||
|
|
432fdd628c | ||
|
|
bc23dd37be | ||
|
|
0319fd23c5 | ||
|
|
d7e5993501 | ||
|
|
46a9b90ed0 | ||
|
|
b0c68e58c5 | ||
|
|
7063ced7fd | ||
|
|
7d2444868d | ||
|
|
0899d42967 | ||
|
|
d57bcc2701 | ||
|
|
d9d92c8766 | ||
|
|
7a16ed5400 | ||
|
|
07e35ff81a | ||
|
|
0398944cab | ||
|
|
a33ff7479a | ||
|
|
f9182e5453 | ||
|
|
4f0a965573 | ||
|
|
2a23487163 | ||
|
|
6b9ba7367d | ||
|
|
a17ae5546f | ||
|
|
47de0e9156 | ||
|
|
baeda622de | ||
|
|
48220b67ed | ||
|
|
9ba232249b | ||
|
|
b0580e70d2 | ||
|
|
6c5b274792 | ||
|
|
3094b08c5f | ||
|
|
181539baac | ||
|
|
2289773e36 | ||
|
|
1ecb138ec5 | ||
|
|
3d67d9eba3 | ||
|
|
0fd3c03764 | ||
|
|
6aca1d0d54 | ||
|
|
601bbfd88e | ||
|
|
f600b0522c | ||
|
|
647a33ea61 | ||
|
|
64d59fedc8 | ||
|
|
51d592ba0d | ||
|
|
eaaf841a87 | ||
|
|
c6542e383c | ||
|
|
6e3c2bfd6a | ||
|
|
be3bfc7aa4 | ||
|
|
bbe90c1683 | ||
|
|
2fe1d04eb0 | ||
|
|
f5022f4e1e | ||
|
|
fdbb06de19 | ||
|
|
0cd4980f44 | ||
|
|
2d0f14d078 | ||
|
|
9711068f8b | ||
|
|
fb180c7b9b | ||
|
|
5947bd6d74 | ||
|
|
7e584402ea | ||
|
|
3f113da056 | ||
|
|
bfef3a96c8 | ||
|
|
de3a467697 | ||
|
|
0f895fd3a1 | ||
|
|
16cc3adcff | ||
|
|
e2e002b9a9 | ||
|
|
8274284294 | ||
|
|
1d7f574b9b | ||
|
|
f680832f78 | ||
|
|
77711ea711 | ||
|
|
f1a6122699 | ||
|
|
5fec881c39 | ||
|
|
5dc05129ef | ||
|
|
f461ad6d31 | ||
|
|
57b5db4f43 | ||
|
|
d015fe5160 | ||
|
|
f7e3f4a828 | ||
|
|
f3b8d66f4f | ||
|
|
8ae03dd1ff | ||
|
|
0e6f6ddbda | ||
|
|
882c503fa9 | ||
|
|
22eb6c6a8d | ||
|
|
8e9ff46bab | ||
|
|
023d8ad893 | ||
|
|
6b730b7c40 | ||
|
|
90cea56a1e | ||
|
|
5e43d9b6b7 | ||
|
|
e4cac86690 | ||
|
|
a249289211 | ||
|
|
8ecfd9780f | ||
|
|
913cd2b3d4 | ||
|
|
c02b7a33fe | ||
|
|
7a0b2060d4 | ||
|
|
afe9056725 | ||
|
|
49be2ad013 | ||
|
|
230ec51de5 | ||
|
|
b37ea482d3 | ||
|
|
bf69c8ce46 | ||
|
|
d2741af24b | ||
|
|
f04f58ac88 | ||
|
|
8757dbb664 | ||
|
|
ffc7f9706d | ||
|
|
4487c3dc1a | ||
|
|
97f5d8e7e2 | ||
|
|
1cc6e09953 | ||
|
|
3752530f96 | ||
|
|
21be35bc46 | ||
|
|
bb8ec4b2ef | ||
|
|
278ea184cc | ||
|
|
0b17a85c3b | ||
|
|
0be0e9792f | ||
|
|
14409ff5b7 | ||
|
|
d24bc3c07c | ||
|
|
5ab419534c | ||
|
|
07b65f37db | ||
|
|
8ad5280501 | ||
|
|
69df6179bb | ||
|
|
b939ae6ab4 | ||
|
|
101a364a83 | ||
|
|
412b7595d2 | ||
|
|
a7ab652dd3 | ||
|
|
d41a4cf78b | ||
|
|
785ed6f9db | ||
|
|
6885abd234 | ||
|
|
3497cb892e | ||
|
|
a82561355c | ||
|
|
f054cdc9ef | ||
|
|
463c7eae54 | ||
|
|
cbb703e5c1 | ||
|
|
e4dc1884f8 | ||
|
|
f72a2b7ef8 | ||
|
|
39819c5c58 | ||
|
|
49542c49fa | ||
|
|
86e501f0aa | ||
|
|
2ca3a784e2 | ||
|
|
c01bd57ba5 | ||
|
|
ba5d224080 | ||
|
|
5da16db81b | ||
|
|
a9704b110d | ||
|
|
b8f048d96a | ||
|
|
c20a285003 | ||
|
|
07cf1141c5 | ||
|
|
ef2aa2ea6f | ||
|
|
2058e0d3fb | ||
|
|
773711a2d5 | ||
|
|
0bb85bc895 | ||
|
|
9a9986cf17 | ||
|
|
1bb62bfc05 | ||
|
|
f92f89e8e8 | ||
|
|
b1a50aa0e0 | ||
|
|
8c2a2fc043 | ||
|
|
adb39fd820 | ||
|
|
8a9762dd93 | ||
|
|
4407da9364 | ||
|
|
b533e4d093 | ||
|
|
239ec5fb53 | ||
|
|
d974d5dc52 | ||
|
|
2076949289 | ||
|
|
65bd7fd64f | ||
|
|
1f0c7297ce | ||
|
|
efbd97f9a4 | ||
|
|
2ccfccc23f | ||
|
|
acbcb6bd45 | ||
|
|
e580dbe7f2 | ||
|
|
9f55678cb3 | ||
|
|
9c2b85dd6e | ||
|
|
cb640c2e71 | ||
|
|
56bdb6e352 | ||
|
|
81e1e5be8f | ||
|
|
6c44a92e2c | ||
|
|
9596f737e8 | ||
|
|
ad5f815273 | ||
|
|
4a893d96a0 | ||
|
|
59a681fcb7 | ||
|
|
787ea885cc | ||
|
|
a26a37233b | ||
|
|
c1e3259b08 | ||
|
|
9c735bb088 | ||
|
|
10092dcadf | ||
|
|
d31cea70bc | ||
|
|
b04ab6faa1 | ||
|
|
23163b3095 | ||
|
|
849d7d2d95 | ||
|
|
a58a324073 | ||
|
|
7c2135f444 | ||
|
|
f9719957b0 | ||
|
|
9ce74e2da1 | ||
|
|
14b959b91b | ||
|
|
e2b9893b17 | ||
|
|
54e43758d3 | ||
|
|
92af45d7fd | ||
|
|
5891a6ee7d | ||
|
|
c10e409634 | ||
|
|
6432207bf1 | ||
|
|
935639e5e0 | ||
|
|
cdb2093ea6 | ||
|
|
856ef34964 | ||
|
|
cf19ceb193 | ||
|
|
e5fe2950af | ||
|
|
1ca242405b | ||
|
|
bcbf0ba75a | ||
|
|
4810042373 | ||
|
|
e1c90d74e3 | ||
|
|
984570c55b | ||
|
|
f489d88be4 | ||
|
|
6a84395303 | ||
|
|
a3847ddd2a | ||
|
|
dc0f023754 | ||
|
|
89677577ef | ||
|
|
0922314134 | ||
|
|
c68604d1fe | ||
|
|
372cfdecf4 | ||
|
|
ef40a0ceea | ||
|
|
343d18241b | ||
|
|
2ecb6e0f9e | ||
|
|
38b8e5e7b7 | ||
|
|
058f8d178e | ||
|
|
fbc1a722bd | ||
|
|
727cfe92e3 | ||
|
|
4bcb13486e | ||
|
|
4aa8603ebf | ||
|
|
ba33c8a456 | ||
|
|
6f4cd88988 | ||
|
|
1f2deff6f0 | ||
|
|
575882be5a | ||
|
|
d591c45e4d | ||
|
|
f9b06adc9f | ||
|
|
6cc67dc790 | ||
|
|
c0c7c0f41a | ||
|
|
eb505d4bd7 | ||
|
|
aebd1a1be1 | ||
|
|
447c06d817 | ||
|
|
ce78131258 | ||
|
|
acab465c96 | ||
|
|
4ea83b8bd5 | ||
|
|
094eb632f2 | ||
|
|
03b1e40593 | ||
|
|
2164b629cf | ||
|
|
a081047008 | ||
|
|
2e395c1b0d | ||
|
|
520e03a612 | ||
|
|
a771a44557 | ||
|
|
38bfe8c8de | ||
|
|
7ca2ef4c4c | ||
|
|
de5f02d706 | ||
|
|
6f7ddef4a4 | ||
|
|
d78b5fac73 | ||
|
|
7cf65ba066 | ||
|
|
91966f676a | ||
|
|
5a1ca3855b | ||
|
|
226203143b | ||
|
|
a5304115f0 | ||
|
|
f7458b8d41 | ||
|
|
410b66d40f | ||
|
|
1fcf510278 | ||
|
|
bb4ce278b0 | ||
|
|
6dac48e5b8 | ||
|
|
7178d208d3 | ||
|
|
00935c86d0 | ||
|
|
3dde78cadf | ||
|
|
630214ddb9 | ||
|
|
b3f8781646 | ||
|
|
b717402d26 | ||
|
|
0d339e0cba | ||
|
|
1d014bf6e3 | ||
|
|
5ab15dc27f | ||
|
|
c347be6f35 | ||
|
|
d47c2a6fe0 | ||
|
|
82eb33a7d4 | ||
|
|
4f6bae193d | ||
|
|
b8752c4158 | ||
|
|
b6d0d777bf | ||
|
|
8c155d4d0e | ||
|
|
bdf5d0f5c6 | ||
|
|
4959b861bd | ||
|
|
a4fa0ae64b | ||
|
|
7cd5f36c7a | ||
|
|
ecfdb16957 | ||
|
|
d31195fc87 | ||
|
|
f3ef4cef74 | ||
|
|
ec6db9c8ca | ||
|
|
bb483c9d72 | ||
|
|
414448137a | ||
|
|
d0acef3ecb | ||
|
|
5617416932 | ||
|
|
bf0eb798d9 | ||
|
|
8afc3812b7 | ||
|
|
5a7841e6bf | ||
|
|
2758e86fab | ||
|
|
d9935a714e | ||
|
|
d6a9d6829b | ||
|
|
8b02371786 | ||
|
|
9a5b692204 | ||
|
|
0b504c7df2 | ||
|
|
6cab3bbc8e | ||
|
|
35194cf345 | ||
|
|
13c5724d7c | ||
|
|
156ea62ffa | ||
|
|
22693bcbcc | ||
|
|
ba70220659 | ||
|
|
fc96d33d6a | ||
|
|
685915e13c | ||
|
|
c39b17f12c | ||
|
|
110d9a4cc1 | ||
|
|
8902328b30 | ||
|
|
7ff9211dfc | ||
|
|
17b4f873e7 | ||
|
|
741c0c08b9 | ||
|
|
c42d9385d1 | ||
|
|
8cbd667286 | ||
|
|
8bf60d502a | ||
|
|
9f60499a3f | ||
|
|
56a9ff2b35 | ||
|
|
8c7b62509b | ||
|
|
5e61065b64 | ||
|
|
39dd0524f8 | ||
|
|
25c6a4d3a6 | ||
|
|
0b9a4c56a9 | ||
|
|
faa08f9e1f | ||
|
|
7fbe0937df | ||
|
|
66f5e34d52 | ||
|
|
ddf59c8d5c | ||
|
|
0856073e85 | ||
|
|
06aef18d0c | ||
|
|
be63648238 | ||
|
|
56e01e66fb | ||
|
|
772153e58a | ||
|
|
3882a5aa89 | ||
|
|
edf8027bf4 | ||
|
|
2fd459381d | ||
|
|
76e67d27e7 | ||
|
|
18be134ad8 | ||
|
|
1feb9f6a27 | ||
|
|
337a760e73 | ||
|
|
54cd412107 | ||
|
|
cf2171ece1 | ||
|
|
47fb8a5513 | ||
|
|
06bf134bd4 | ||
|
|
cf8899fcbe | ||
|
|
c05b77961e | ||
|
|
19c365cd12 | ||
|
|
29f032087e | ||
|
|
d0cb7b9724 | ||
|
|
ad162677a6 | ||
|
|
fbbbe7d17d | ||
|
|
ef0d11c042 | ||
|
|
fc2608980f | ||
|
|
cc97e82a78 | ||
|
|
54e3191de6 | ||
|
|
4f8c8762c7 | ||
|
|
c190f1986e | ||
|
|
b418048bc9 | ||
|
|
0fdd1c74f2 | ||
|
|
3b1b2b95e7 | ||
|
|
3bb5484b7f | ||
|
|
d93c09b27b | ||
|
|
e7ec18d270 | ||
|
|
3ebe21e135 | ||
|
|
aca1ecf1ee | ||
|
|
e8ef2fdc2c | ||
|
|
b129d5fb08 | ||
|
|
11f4564465 | ||
|
|
c9d140281b | ||
|
|
fa637a37d5 | ||
|
|
1589c3fc51 | ||
|
|
bdc2b31202 | ||
|
|
0970e1e33c | ||
|
|
028003dffc | ||
|
|
1eb4ac7f34 | ||
|
|
d97e356376 | ||
|
|
d36352af16 | ||
|
|
05ae92d5f8 | ||
|
|
dce612f944 | ||
|
|
3a196203c3 | ||
|
|
33578a6289 | ||
|
|
4c3db2119b | ||
|
|
4a7ff3cd94 | ||
|
|
5578580d78 | ||
|
|
dc1d8366a5 | ||
|
|
252f0692c8 | ||
|
|
9d13925280 | ||
|
|
5462a71f52 | ||
|
|
42953a0b62 | ||
|
|
f146a1d80f | ||
|
|
a113c71de7 | ||
|
|
1f642f436a | ||
|
|
62d27a17d5 | ||
|
|
35941a58a5 | ||
|
|
2ace2165e0 | ||
|
|
1cfcacfa9a | ||
|
|
e020fd1154 | ||
|
|
1dcc645fec | ||
|
|
87fba75860 | ||
|
|
294360d35a | ||
|
|
a7684d7206 | ||
|
|
af81ede100 | ||
|
|
698beedaa2 | ||
|
|
a6b4cce7f3 | ||
|
|
ba66ff840f | ||
|
|
c296f33ba1 | ||
|
|
e7a49192bd | ||
|
|
5774d913af | ||
|
|
b068db3f7a | ||
|
|
8e49241e7c | ||
|
|
b8cee5cc9c | ||
|
|
794808d3a7 | ||
|
|
48f6d1dfec | ||
|
|
97e1aae9c0 | ||
|
|
e2511c5ed3 | ||
|
|
74bdfc8c2d | ||
|
|
fbccf23d36 | ||
|
|
906aaa15a3 | ||
|
|
0ae1f9c754 | ||
|
|
f1bd89fd02 | ||
|
|
3949b47e51 | ||
|
|
2f6595bca7 | ||
|
|
3bcd0ddc46 | ||
|
|
ca93c2cfcd | ||
|
|
a633e3c553 | ||
|
|
bef2731207 | ||
|
|
ee53ee4077 | ||
|
|
34bfc12647 | ||
|
|
3b425c3e14 | ||
|
|
69eb007ea2 | ||
|
|
90c3350d40 | ||
|
|
a7ddbd72b3 | ||
|
|
5a2ee98ae2 | ||
|
|
ea0b5d5e26 | ||
|
|
af2cb1be1a | ||
|
|
c30e7ac683 | ||
|
|
7fb5ac11fd | ||
|
|
b2dc0ac819 | ||
|
|
dbdf873ba4 | ||
|
|
1b70b6e88c | ||
|
|
c90e13d35e | ||
|
|
442375f76e | ||
|
|
81d493e1d6 | ||
|
|
151f16af47 | ||
|
|
606a220603 | ||
|
|
362e758c40 | ||
|
|
2eb3a55f59 | ||
|
|
6720c03cbc | ||
|
|
a76386b53b | ||
|
|
0243632357 | ||
|
|
bb24b55a67 | ||
|
|
f47fd8eec4 | ||
|
|
f1f9f13d82 | ||
|
|
d2dd82c0ec | ||
|
|
e1738b625d | ||
|
|
7aa37183b6 | ||
|
|
c91b28a850 | ||
|
|
70225c1a18 | ||
|
|
3d9d7d899d | ||
|
|
f0619c7d13 | ||
|
|
305fa84d38 | ||
|
|
edf0e2bedb | ||
|
|
8be5561d19 | ||
|
|
f11ca53282 | ||
|
|
2c25d6cc0a | ||
|
|
db6ab4d8ec | ||
|
|
3961eff372 | ||
|
|
458a7827f9 | ||
|
|
68d1c77a79 | ||
|
|
aa97e30d51 | ||
|
|
9027d7d391 | ||
|
|
974fd5de0f | ||
|
|
f9d28fbf83 | ||
|
|
b944089087 | ||
|
|
7b6cf28459 | ||
|
|
be91688efb | ||
|
|
a5d47231aa | ||
|
|
01e833a399 | ||
|
|
c363ba8056 | ||
|
|
7cec39ba6c | ||
|
|
3f15cbd2bd | ||
|
|
3235d33463 | ||
|
|
140597c7f8 | ||
|
|
e1407a7d73 | ||
|
|
03525c010f | ||
|
|
8dc202af92 | ||
|
|
369977f8f3 | ||
|
|
7f8c092dfc | ||
|
|
d517cad6e6 | ||
|
|
62a68890d3 | ||
|
|
3d8a8cc77b | ||
|
|
55dc35a8fc | ||
|
|
82e49a5e44 | ||
|
|
17ac6f96a0 | ||
|
|
085db3e0a6 | ||
|
|
348bebc417 | ||
|
|
15d21cc673 | ||
|
|
7e0ff14f28 | ||
|
|
67d09e8b3d | ||
|
|
ce3b53a920 | ||
|
|
a32809e14b | ||
|
|
1d8c515da2 | ||
|
|
81e0f1a025 | ||
|
|
c593e2789c | ||
|
|
650d2d7a47 | ||
|
|
2665c86683 | ||
|
|
8b262f3424 | ||
|
|
5187f3b84f | ||
|
|
443e083a79 | ||
|
|
6c262c20ce | ||
|
|
cfbf2903c1 | ||
|
|
19b8ff7d9f | ||
|
|
ec6ffd2115 | ||
|
|
433b1e2979 | ||
|
|
bd3d079dfb | ||
|
|
fe776191b7 | ||
|
|
c546d8787d | ||
|
|
a672b84b88 | ||
|
|
e3a137c675 | ||
|
|
10aa99abdc | ||
|
|
34567d451f | ||
|
|
494e3dc62c | ||
|
|
0997274f29 | ||
|
|
76161329b6 | ||
|
|
8505750958 | ||
|
|
4077105db1 | ||
|
|
3f31d83a55 | ||
|
|
d729e3c567 | ||
|
|
9af75f9a43 | ||
|
|
d32d334a2e | ||
|
|
94006a843c | ||
|
|
4790590327 | ||
|
|
7cf7763e21 | ||
|
|
0d7979a72f | ||
|
|
300425e698 | ||
|
|
59010baf89 | ||
|
|
47fcb122a2 | ||
|
|
bbb50b1397 | ||
|
|
ae8724d699 | ||
|
|
2169f6979d | ||
|
|
9cc577e9c7 | ||
|
|
6ead58f62f | ||
|
|
ec3118227c | ||
|
|
0d3d9bc78a | ||
|
|
e16b3db0d4 | ||
|
|
cdab874b5b | ||
|
|
bf40995b16 | ||
|
|
68b3a4fbb7 | ||
|
|
c38bfa1101 | ||
|
|
af7a85eeb7 | ||
|
|
2bd5dc21a8 | ||
|
|
18a151c8e8 | ||
|
|
da19a1a9c6 | ||
|
|
45cdb5a3e4 | ||
|
|
ab19dbc35e | ||
|
|
6a443734a1 | ||
|
|
f0251d3056 | ||
|
|
31127ccf29 | ||
|
|
b97e055b39 | ||
|
|
f4ce1ee0fa | ||
|
|
2a29311ca5 | ||
|
|
8cfd7ee170 | ||
|
|
59a8354a7f | ||
|
|
f443942e03 | ||
|
|
772208ba22 | ||
|
|
e46a1be5d7 | ||
|
|
d295a9d0e4 | ||
|
|
f0bf34073b | ||
|
|
73098d106d | ||
|
|
c8ea4cd85e | ||
|
|
f64ddf46e2 | ||
|
|
5e6e3dd793 | ||
|
|
13ff59ec89 | ||
|
|
7cc3fc728b | ||
|
|
ec79e12bf3 | ||
|
|
d38b989c02 | ||
|
|
f9a629bf8d | ||
|
|
02edbce460 | ||
|
|
b9f84d012f | ||
|
|
421478bb49 | ||
|
|
cde106bd77 | ||
|
|
144f00e2cc | ||
|
|
74185f0beb | ||
|
|
09f238162e | ||
|
|
13ece650bc | ||
|
|
cbef262805 | ||
|
|
cbdd641d5f | ||
|
|
f435c38aa5 | ||
|
|
4e135681bc | ||
|
|
8da969455b | ||
|
|
f4ebf77b36 | ||
|
|
bb3af18e46 | ||
|
|
c025df2974 | ||
|
|
5cf5e6ec6c | ||
|
|
6236252a90 | ||
|
|
9f71d9331c | ||
|
|
488e6d09ca | ||
|
|
00b1fbe091 | ||
|
|
c1ea1bb54f | ||
|
|
49a5d922fc | ||
|
|
a960084438 | ||
|
|
8ae44e6f7f | ||
|
|
95ea220936 | ||
|
|
7ffdb3ddc7 | ||
|
|
decbbc9acd | ||
|
|
496e05651e | ||
|
|
efd36bf207 | ||
|
|
283668ef18 | ||
|
|
310299367b | ||
|
|
63c7c55843 | ||
|
|
4bc83b01d3 | ||
|
|
743c3ab784 | ||
|
|
98d2b0b889 | ||
|
|
c39417c93d | ||
|
|
1b2b62f04c | ||
|
|
1a31855fc8 | ||
|
|
f4e92dedff | ||
|
|
85d6be64cc | ||
|
|
033e058745 | ||
|
|
1cbe0b7b9f | ||
|
|
22433d1a39 | ||
|
|
e048060c72 | ||
|
|
60d04b5c58 | ||
|
|
0a991ecdf8 | ||
|
|
a9f29a3151 | ||
|
|
50f417a7e2 | ||
|
|
d69492988a | ||
|
|
286fa14a30 | ||
|
|
8fa9b15fbe | ||
|
|
b432324159 | ||
|
|
255ea41648 | ||
|
|
6bcfdfaaf8 |
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "airbnb-base",
|
||||
"extends": ["airbnb-base", "prettier"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "2020",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
"modules": true
|
||||
@@ -8,7 +9,8 @@
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"pragma": "h"
|
||||
"pragma": "h",
|
||||
"version": "15.0"
|
||||
},
|
||||
"import/resolver": {
|
||||
"webpack": {
|
||||
@@ -21,7 +23,7 @@
|
||||
"__DEMO__": false,
|
||||
"__BUILD__": false,
|
||||
"__VERSION__": false,
|
||||
"__PUBLIC_PATH__": false,
|
||||
"__STATIC_PATH__": false,
|
||||
"Polymer": true,
|
||||
"webkitSpeechRecognition": false,
|
||||
"ResizeObserver": false
|
||||
@@ -68,13 +70,11 @@
|
||||
"react/no-find-dom-node": 2,
|
||||
"react/no-is-mounted": 2,
|
||||
"react/jsx-no-comment-textnodes": 2,
|
||||
"react/jsx-curly-spacing": 2,
|
||||
"react/jsx-no-undef": 2,
|
||||
"react/jsx-uses-react": 2,
|
||||
"react/jsx-uses-vars": 2,
|
||||
"no-restricted-syntax": [0, "ForOfStatement"]
|
||||
"no-restricted-syntax": [0, "ForOfStatement"],
|
||||
"prettier/prettier": "error"
|
||||
},
|
||||
"plugins": [
|
||||
"react"
|
||||
]
|
||||
"plugins": ["react", "prettier"]
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
{
|
||||
"extends": "./.eslintrc-hound.json",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"plugins": ["react"],
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"rules": {
|
||||
"import/no-unresolved": 2,
|
||||
"linebreak-style": 0
|
||||
"linebreak-style": 0,
|
||||
"implicit-arrow-linebreak": 0
|
||||
}
|
||||
}
|
||||
|
||||
13
.gitattributes
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Ensure Docker script files uses LF to support Docker for Windows.
|
||||
# Ensure "git config --global core.autocrlf input" before you clone
|
||||
* text eol=lf
|
||||
*.ts whitespace=error
|
||||
*.js whitespace=error
|
||||
|
||||
*.ico binary
|
||||
*.jpg binary
|
||||
*.png binary
|
||||
*.zip binary
|
||||
*.mp3 binary
|
||||
|
||||
demo/public/api/camera_proxy_stream/* binary
|
||||
4
.github/ISSUE_TEMPLATE.md
vendored
@@ -13,6 +13,10 @@
|
||||
|
||||
**Last working Home Assistant release (if known):**
|
||||
|
||||
**UI (States or Lovelace UI?):**
|
||||
<!--
|
||||
- Frontend -> Developer tools -> Info
|
||||
-->
|
||||
|
||||
**Browser and Operating System:**
|
||||
<!--
|
||||
|
||||
9
.gitignore
vendored
@@ -4,8 +4,8 @@ node_modules/*
|
||||
npm-debug.log
|
||||
.DS_Store
|
||||
hass_frontend/*
|
||||
hass_frontend_es5/*
|
||||
.reify-cache
|
||||
demo/hademo-icons.html
|
||||
|
||||
# Python stuff
|
||||
*.py[cod]
|
||||
@@ -21,6 +21,13 @@ lib
|
||||
bin
|
||||
dist
|
||||
|
||||
# vscode
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
|
||||
# Cast dev settings
|
||||
src/cast/dev_const.ts
|
||||
|
||||
# Secrets
|
||||
.lokalise_token
|
||||
yarn-error.log
|
||||
|
||||
9
.vscode/extensions.json
vendored
Executable file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"ms-vscode.vscode-typescript-tslint-plugin",
|
||||
"esbenp.prettier-vscode",
|
||||
"bierner.lit-html",
|
||||
"runem.lit-plugin"
|
||||
]
|
||||
}
|
||||
28
Dockerfile
@@ -1,25 +1,31 @@
|
||||
FROM node:8.2.1-alpine
|
||||
FROM node:8.11.1-alpine
|
||||
|
||||
# install yarn
|
||||
ENV PATH /root/.yarn/bin:$PATH
|
||||
|
||||
## Install/force base tools
|
||||
RUN apk update \
|
||||
&& apk add curl bash binutils tar git python3 \
|
||||
&& apk add make g++ curl bash binutils tar git python2 python3 \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& /bin/bash \
|
||||
&& touch ~/.bashrc \
|
||||
&& curl -o- -L https://yarnpkg.com/install.sh | bash
|
||||
&& touch ~/.bashrc
|
||||
|
||||
## Install yarn
|
||||
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
|
||||
|
||||
## Setup the project
|
||||
RUN mkdir -p /frontend
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
ENV NODE_ENV production
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
COPY package.json ./
|
||||
RUN yarn
|
||||
|
||||
COPY bower.json ./
|
||||
RUN ./node_modules/.bin/bower install --allow-root
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
CMD [ "/bin/bash", "./script/build_frontend" ]
|
||||
|
||||
COPY script/docker_entrypoint.sh /usr/bin/docker_entrypoint.sh
|
||||
|
||||
RUN chmod +x /usr/bin/docker_entrypoint.sh
|
||||
|
||||
CMD [ "docker_entrypoint.sh" ]
|
||||
|
||||
12
README.md
@@ -16,6 +16,18 @@ This is the repository for the official [Home Assistant](https://home-assistant.
|
||||
- Gallery: `cd gallery && script/develop_gallery`
|
||||
- Hass.io: [Instructions](https://developers.home-assistant.io/docs/en/hassio_hass.html)
|
||||
|
||||
## Frontend development
|
||||
|
||||
### Classic environment
|
||||
A complete guide can be found at the following [link](https://www.home-assistant.io/developers/frontend/). It describes a short guide for the build of project.
|
||||
|
||||
### Docker environment
|
||||
It is possible to compile the project and/or run commands in the development environment having only the [Docker](https://www.docker.com) pre-installed in the system. On the root of project you can do:
|
||||
* `sh ./script/docker_run.sh build` Build all the project with one command
|
||||
* `sh ./script/docker_run.sh bash` Open an interactive shell (the same environment generated by the *classic environment*) where you can run commands. This bash work on your project directory and any change on your file is automatically present within your build bash.
|
||||
|
||||
**Note**: if you have installed `npm` in addition to the `docker`, you can use the commands `npm run docker_build` and `npm run bash` to get a full build or bash as explained above
|
||||
|
||||
## License
|
||||
|
||||
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
|
||||
|
||||
64
azure-pipelines-release.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
# https://dev.azure.com/home-assistant
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
tags:
|
||||
include:
|
||||
- "*"
|
||||
pr: none
|
||||
variables:
|
||||
- name: versionWheels
|
||||
value: '1.3-3.7-alpine3.10'
|
||||
- name: versionNode
|
||||
value: '12.1'
|
||||
- group: twine
|
||||
resources:
|
||||
repositories:
|
||||
- repository: azure
|
||||
type: github
|
||||
name: 'home-assistant/ci-azure'
|
||||
endpoint: 'home-assistant'
|
||||
|
||||
|
||||
stages:
|
||||
- stage: "Validate"
|
||||
jobs:
|
||||
- template: templates/azp-job-version.yaml@azure
|
||||
|
||||
- stage: "Build"
|
||||
jobs:
|
||||
- job: "ReleasePython"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
displayName: "Use Python 3.7"
|
||||
inputs:
|
||||
versionSpec: "3.7"
|
||||
- task: NodeTool@0
|
||||
displayName: "Use Node $(versionNode)"
|
||||
inputs:
|
||||
versionSpec: "$(versionNode)"
|
||||
- script: pip install twine wheel
|
||||
displayName: "Install tools"
|
||||
- script: |
|
||||
export TWINE_USERNAME="$(twineUser)"
|
||||
export TWINE_PASSWORD="$(twinePassword)"
|
||||
|
||||
script/release
|
||||
displayName: "Build and release package"
|
||||
- template: templates/azp-job-wheels.yaml@azure
|
||||
parameters:
|
||||
builderVersion: '$(versionWheels)'
|
||||
builderApk: 'build-base'
|
||||
wheelsLocal: true
|
||||
preBuild:
|
||||
- task: NodeTool@0
|
||||
displayName: "Use Node $(versionNode)"
|
||||
inputs:
|
||||
versionSpec: "$(versionNode)"
|
||||
- script: |
|
||||
set -e
|
||||
|
||||
yarn install
|
||||
script/build_frontend
|
||||
7
build-scripts/.eslintrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../.eslintrc.json",
|
||||
"rules": {
|
||||
"import/no-extraneous-dependencies": 0,
|
||||
"global-require": 0
|
||||
}
|
||||
}
|
||||
51
build-scripts/gulp/app.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// Run HA develop mode
|
||||
const gulp = require("gulp");
|
||||
|
||||
require("./clean.js");
|
||||
require("./translations.js");
|
||||
require("./gen-icons.js");
|
||||
require("./gather-static.js");
|
||||
require("./webpack.js");
|
||||
require("./service-worker.js");
|
||||
require("./entry-html.js");
|
||||
|
||||
gulp.task(
|
||||
"develop-app",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean",
|
||||
gulp.parallel(
|
||||
"gen-service-worker-dev",
|
||||
"gen-icons",
|
||||
"gen-pages-dev",
|
||||
"gen-index-app-dev",
|
||||
gulp.series("create-test-translation", "build-translations")
|
||||
),
|
||||
"copy-static",
|
||||
"webpack-watch-app"
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-app",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean",
|
||||
gulp.parallel("gen-icons", "build-translations"),
|
||||
"copy-static",
|
||||
gulp.parallel(
|
||||
"webpack-prod-app",
|
||||
// Do not compress static files in CI, it's SLOW.
|
||||
...(process.env.CI === "true" ? [] : ["compress-static"])
|
||||
),
|
||||
gulp.parallel(
|
||||
"gen-pages-prod",
|
||||
"gen-index-app-prod",
|
||||
"gen-service-worker-prod"
|
||||
)
|
||||
)
|
||||
);
|
||||
37
build-scripts/gulp/cast.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// Run cast develop mode
|
||||
const gulp = require("gulp");
|
||||
|
||||
require("./clean.js");
|
||||
require("./translations.js");
|
||||
require("./gen-icons.js");
|
||||
require("./gather-static.js");
|
||||
require("./webpack.js");
|
||||
require("./service-worker.js");
|
||||
require("./entry-html.js");
|
||||
|
||||
gulp.task(
|
||||
"develop-cast",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-cast",
|
||||
gulp.parallel("gen-icons", "gen-index-cast-dev", "build-translations"),
|
||||
"copy-static-cast",
|
||||
"webpack-dev-server-cast"
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-cast",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-cast",
|
||||
gulp.parallel("gen-icons", "build-translations"),
|
||||
"copy-static-cast",
|
||||
"webpack-prod-cast",
|
||||
"gen-index-cast-prod"
|
||||
)
|
||||
);
|
||||
23
build-scripts/gulp/clean.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const del = require("del");
|
||||
const gulp = require("gulp");
|
||||
const config = require("../paths");
|
||||
require("./translations");
|
||||
|
||||
gulp.task(
|
||||
"clean",
|
||||
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
|
||||
return del([config.root, config.build_dir]);
|
||||
})
|
||||
);
|
||||
gulp.task(
|
||||
"clean-demo",
|
||||
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
|
||||
return del([config.demo_root, config.build_dir]);
|
||||
})
|
||||
);
|
||||
gulp.task(
|
||||
"clean-cast",
|
||||
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
|
||||
return del([config.cast_root, config.build_dir]);
|
||||
})
|
||||
);
|
||||
42
build-scripts/gulp/demo.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// Run demo develop mode
|
||||
const gulp = require("gulp");
|
||||
|
||||
require("./clean.js");
|
||||
require("./translations.js");
|
||||
require("./gen-icons.js");
|
||||
require("./gather-static.js");
|
||||
require("./webpack.js");
|
||||
require("./service-worker.js");
|
||||
require("./entry-html.js");
|
||||
|
||||
gulp.task(
|
||||
"develop-demo",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-demo",
|
||||
gulp.parallel(
|
||||
"gen-icons",
|
||||
"gen-icons-demo",
|
||||
"gen-index-demo-dev",
|
||||
"build-translations"
|
||||
),
|
||||
"copy-static-demo",
|
||||
"webpack-dev-server-demo"
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-demo",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-demo",
|
||||
gulp.parallel("gen-icons", "gen-icons-demo", "build-translations"),
|
||||
"copy-static-demo",
|
||||
"webpack-prod-demo",
|
||||
"gen-index-demo-prod"
|
||||
)
|
||||
);
|
||||
73
build-scripts/gulp/download_translations.js
Normal file
@@ -0,0 +1,73 @@
|
||||
const del = require("del");
|
||||
const gulp = require("gulp");
|
||||
const mapStream = require("map-stream");
|
||||
|
||||
const inDir = "translations";
|
||||
const downloadDir = inDir + "/downloads";
|
||||
|
||||
const tasks = [];
|
||||
|
||||
function hasHtml(data) {
|
||||
return /<[a-z][\s\S]*>/i.test(data);
|
||||
}
|
||||
|
||||
function recursiveCheckHasHtml(file, data, errors, recKey) {
|
||||
Object.keys(data).forEach(function(key) {
|
||||
if (typeof data[key] === "object") {
|
||||
nextRecKey = recKey ? `${recKey}.${key}` : key;
|
||||
recursiveCheckHasHtml(file, data[key], errors, nextRecKey);
|
||||
} else if (hasHtml(data[key])) {
|
||||
errors.push(`HTML found in ${file.path} at key ${recKey}.${key}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function checkHtml() {
|
||||
let errors = [];
|
||||
|
||||
return mapStream(function(file, cb) {
|
||||
const content = file.contents;
|
||||
let error;
|
||||
if (content) {
|
||||
if (hasHtml(String(content))) {
|
||||
const data = JSON.parse(String(content));
|
||||
recursiveCheckHasHtml(file, data, errors);
|
||||
if (errors.length > 0) {
|
||||
error = errors.join("\r\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
cb(error, file);
|
||||
});
|
||||
}
|
||||
|
||||
let taskName = "clean-downloaded-translations";
|
||||
gulp.task(taskName, function() {
|
||||
return del([`${downloadDir}/**`]);
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = "check-translations-html";
|
||||
gulp.task(taskName, function() {
|
||||
return gulp.src(`${downloadDir}/*.json`).pipe(checkHtml());
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = "move-downloaded-translations";
|
||||
gulp.task(taskName, function() {
|
||||
return gulp.src(`${downloadDir}/*.json`).pipe(gulp.dest(inDir));
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = "check-downloaded-translations";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series(
|
||||
"check-translations-html",
|
||||
"move-downloaded-translations",
|
||||
"clean-downloaded-translations"
|
||||
)
|
||||
);
|
||||
tasks.push(taskName);
|
||||
|
||||
module.exports = tasks;
|
||||
216
build-scripts/gulp/entry-html.js
Normal file
@@ -0,0 +1,216 @@
|
||||
// Tasks to generate entry HTML
|
||||
/* eslint-disable import/no-dynamic-require */
|
||||
/* eslint-disable global-require */
|
||||
const gulp = require("gulp");
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const template = require("lodash.template");
|
||||
const minify = require("html-minifier").minify;
|
||||
const config = require("../paths.js");
|
||||
|
||||
const templatePath = (tpl) =>
|
||||
path.resolve(config.polymer_dir, "src/html/", `${tpl}.html.template`);
|
||||
|
||||
const demoTemplatePath = (tpl) =>
|
||||
path.resolve(config.demo_dir, "src/html/", `${tpl}.html.template`);
|
||||
|
||||
const castTemplatePath = (tpl) =>
|
||||
path.resolve(config.cast_dir, "src/html/", `${tpl}.html.template`);
|
||||
|
||||
const readFile = (pth) => fs.readFileSync(pth).toString();
|
||||
|
||||
const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
|
||||
const compiled = template(readFile(pathFunc(pth)));
|
||||
return compiled({ ...data, renderTemplate });
|
||||
};
|
||||
|
||||
const renderDemoTemplate = (pth, data = {}) =>
|
||||
renderTemplate(pth, data, demoTemplatePath);
|
||||
|
||||
const renderCastTemplate = (pth, data = {}) =>
|
||||
renderTemplate(pth, data, castTemplatePath);
|
||||
|
||||
const minifyHtml = (content) =>
|
||||
minify(content, {
|
||||
collapseWhitespace: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
removeComments: true,
|
||||
});
|
||||
|
||||
const PAGES = ["onboarding", "authorize"];
|
||||
|
||||
gulp.task("gen-pages-dev", (done) => {
|
||||
for (const page of PAGES) {
|
||||
const content = renderTemplate(page, {
|
||||
latestPageJS: `/frontend_latest/${page}.js`,
|
||||
latestHassIconsJS: "/frontend_latest/hass-icons.js",
|
||||
|
||||
es5Compatibility: "/frontend_es5/compatibility.js",
|
||||
es5PageJS: `/frontend_es5/${page}.js`,
|
||||
es5HassIconsJS: "/frontend_es5/hass-icons.js",
|
||||
});
|
||||
|
||||
fs.outputFileSync(path.resolve(config.root, `${page}.html`), content);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-pages-prod", (done) => {
|
||||
const latestManifest = require(path.resolve(config.output, "manifest.json"));
|
||||
const es5Manifest = require(path.resolve(config.output_es5, "manifest.json"));
|
||||
|
||||
for (const page of PAGES) {
|
||||
const content = renderTemplate(page, {
|
||||
latestPageJS: latestManifest[`${page}.js`],
|
||||
latestHassIconsJS: latestManifest["hass-icons.js"],
|
||||
|
||||
es5Compatibility: es5Manifest["compatibility.js"],
|
||||
es5PageJS: es5Manifest[`${page}.js`],
|
||||
es5HassIconsJS: es5Manifest["hass-icons.js"],
|
||||
});
|
||||
|
||||
fs.outputFileSync(
|
||||
path.resolve(config.root, `${page}.html`),
|
||||
minifyHtml(content)
|
||||
);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-index-app-dev", (done) => {
|
||||
// In dev mode we don't mangle names, so we hardcode urls. That way we can
|
||||
// run webpack as last in watch mode, which blocks output.
|
||||
const content = renderTemplate("index", {
|
||||
latestAppJS: "/frontend_latest/app.js",
|
||||
latestCoreJS: "/frontend_latest/core.js",
|
||||
latestCustomPanelJS: "/frontend_latest/custom-panel.js",
|
||||
latestHassIconsJS: "/frontend_latest/hass-icons.js",
|
||||
|
||||
es5Compatibility: "/frontend_es5/compatibility.js",
|
||||
es5AppJS: "/frontend_es5/app.js",
|
||||
es5CoreJS: "/frontend_es5/core.js",
|
||||
es5CustomPanelJS: "/frontend_es5/custom-panel.js",
|
||||
es5HassIconsJS: "/frontend_es5/hass-icons.js",
|
||||
}).replace(/#THEMEC/g, "{{ theme_color }}");
|
||||
|
||||
fs.outputFileSync(path.resolve(config.root, "index.html"), content);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-index-app-prod", (done) => {
|
||||
const latestManifest = require(path.resolve(config.output, "manifest.json"));
|
||||
const es5Manifest = require(path.resolve(config.output_es5, "manifest.json"));
|
||||
const content = renderTemplate("index", {
|
||||
latestAppJS: latestManifest["app.js"],
|
||||
latestCoreJS: latestManifest["core.js"],
|
||||
latestCustomPanelJS: latestManifest["custom-panel.js"],
|
||||
latestHassIconsJS: latestManifest["hass-icons.js"],
|
||||
|
||||
es5Compatibility: es5Manifest["compatibility.js"],
|
||||
es5AppJS: es5Manifest["app.js"],
|
||||
es5CoreJS: es5Manifest["core.js"],
|
||||
es5CustomPanelJS: es5Manifest["custom-panel.js"],
|
||||
es5HassIconsJS: es5Manifest["hass-icons.js"],
|
||||
});
|
||||
const minified = minifyHtml(content).replace(/#THEMEC/g, "{{ theme_color }}");
|
||||
|
||||
fs.outputFileSync(path.resolve(config.root, "index.html"), minified);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-index-cast-dev", (done) => {
|
||||
const contentReceiver = renderCastTemplate("receiver", {
|
||||
latestReceiverJS: "/frontend_latest/receiver.js",
|
||||
});
|
||||
fs.outputFileSync(
|
||||
path.resolve(config.cast_root, "receiver.html"),
|
||||
contentReceiver
|
||||
);
|
||||
|
||||
const contentFAQ = renderCastTemplate("launcher-faq", {
|
||||
latestLauncherJS: "/frontend_latest/launcher.js",
|
||||
es5LauncherJS: "/frontend_es5/launcher.js",
|
||||
});
|
||||
fs.outputFileSync(path.resolve(config.cast_root, "faq.html"), contentFAQ);
|
||||
|
||||
const contentLauncher = renderCastTemplate("launcher", {
|
||||
latestLauncherJS: "/frontend_latest/launcher.js",
|
||||
es5LauncherJS: "/frontend_es5/launcher.js",
|
||||
});
|
||||
fs.outputFileSync(
|
||||
path.resolve(config.cast_root, "index.html"),
|
||||
contentLauncher
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-index-cast-prod", (done) => {
|
||||
const latestManifest = require(path.resolve(
|
||||
config.cast_output,
|
||||
"manifest.json"
|
||||
));
|
||||
const es5Manifest = require(path.resolve(
|
||||
config.cast_output_es5,
|
||||
"manifest.json"
|
||||
));
|
||||
|
||||
const contentReceiver = renderCastTemplate("receiver", {
|
||||
latestReceiverJS: latestManifest["receiver.js"],
|
||||
});
|
||||
fs.outputFileSync(
|
||||
path.resolve(config.cast_root, "receiver.html"),
|
||||
contentReceiver
|
||||
);
|
||||
|
||||
const contentFAQ = renderCastTemplate("launcher-faq", {
|
||||
latestLauncherJS: latestManifest["launcher.js"],
|
||||
es5LauncherJS: es5Manifest["launcher.js"],
|
||||
});
|
||||
fs.outputFileSync(path.resolve(config.cast_root, "faq.html"), contentFAQ);
|
||||
|
||||
const contentLauncher = renderCastTemplate("launcher", {
|
||||
latestLauncherJS: latestManifest["launcher.js"],
|
||||
es5LauncherJS: es5Manifest["launcher.js"],
|
||||
});
|
||||
fs.outputFileSync(
|
||||
path.resolve(config.cast_root, "index.html"),
|
||||
contentLauncher
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-index-demo-dev", (done) => {
|
||||
// In dev mode we don't mangle names, so we hardcode urls. That way we can
|
||||
// run webpack as last in watch mode, which blocks output.
|
||||
const content = renderDemoTemplate("index", {
|
||||
latestDemoJS: "/frontend_latest/main.js",
|
||||
|
||||
es5Compatibility: "/frontend_es5/compatibility.js",
|
||||
es5DemoJS: "/frontend_es5/main.js",
|
||||
});
|
||||
|
||||
fs.outputFileSync(path.resolve(config.demo_root, "index.html"), content);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-index-demo-prod", (done) => {
|
||||
const latestManifest = require(path.resolve(
|
||||
config.demo_output,
|
||||
"manifest.json"
|
||||
));
|
||||
const es5Manifest = require(path.resolve(
|
||||
config.demo_output_es5,
|
||||
"manifest.json"
|
||||
));
|
||||
const content = renderDemoTemplate("index", {
|
||||
latestDemoJS: latestManifest["main.js"],
|
||||
|
||||
es5Compatibility: es5Manifest["compatibility.js"],
|
||||
es5DemoJS: es5Manifest["main.js"],
|
||||
});
|
||||
const minified = minifyHtml(content).replace(/#THEMEC/g, "{{ theme_color }}");
|
||||
|
||||
fs.outputFileSync(path.resolve(config.demo_root, "index.html"), minified);
|
||||
done();
|
||||
});
|
||||
128
build-scripts/gulp/gather-static.js
Normal file
@@ -0,0 +1,128 @@
|
||||
// Gulp task to gather all static files.
|
||||
|
||||
const gulp = require("gulp");
|
||||
const path = require("path");
|
||||
const cpx = require("cpx");
|
||||
const fs = require("fs-extra");
|
||||
const zopfli = require("gulp-zopfli-green");
|
||||
const merge = require("merge-stream");
|
||||
const paths = require("../paths");
|
||||
|
||||
const npmPath = (...parts) =>
|
||||
path.resolve(paths.polymer_dir, "node_modules", ...parts);
|
||||
const polyPath = (...parts) => path.resolve(paths.polymer_dir, ...parts);
|
||||
|
||||
const copyFileDir = (fromFile, toDir) =>
|
||||
fs.copySync(fromFile, path.join(toDir, path.basename(fromFile)));
|
||||
|
||||
const genStaticPath = (staticDir) => (...parts) =>
|
||||
path.resolve(staticDir, ...parts);
|
||||
|
||||
function copyTranslations(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
|
||||
// Translation output
|
||||
fs.copySync(
|
||||
polyPath("build-translations/output"),
|
||||
staticPath("translations")
|
||||
);
|
||||
}
|
||||
|
||||
function copyPolyfills(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
|
||||
// Web Component polyfills and adapters
|
||||
copyFileDir(
|
||||
npmPath("@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"),
|
||||
staticPath("polyfills/")
|
||||
);
|
||||
copyFileDir(
|
||||
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js"),
|
||||
staticPath("polyfills/")
|
||||
);
|
||||
copyFileDir(
|
||||
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
|
||||
staticPath("polyfills/")
|
||||
);
|
||||
}
|
||||
|
||||
function copyFonts(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
// Local fonts
|
||||
cpx.copySync(
|
||||
npmPath("roboto-fontface/fonts/roboto/*.woff2"),
|
||||
staticPath("fonts/roboto")
|
||||
);
|
||||
}
|
||||
|
||||
function copyMapPanel(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
copyFileDir(
|
||||
npmPath("leaflet/dist/leaflet.css"),
|
||||
staticPath("images/leaflet/")
|
||||
);
|
||||
fs.copySync(
|
||||
npmPath("leaflet/dist/images"),
|
||||
staticPath("images/leaflet/images/")
|
||||
);
|
||||
}
|
||||
|
||||
function compressStatic(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
const polyfills = gulp
|
||||
.src(staticPath("polyfills/*.js"))
|
||||
.pipe(zopfli())
|
||||
.pipe(gulp.dest(staticPath("polyfills")));
|
||||
const translations = gulp
|
||||
.src(staticPath("translations/*.json"))
|
||||
.pipe(zopfli())
|
||||
.pipe(gulp.dest(staticPath("translations")));
|
||||
|
||||
return merge(polyfills, translations);
|
||||
}
|
||||
|
||||
gulp.task("copy-static", (done) => {
|
||||
const staticDir = paths.static;
|
||||
const staticPath = genStaticPath(paths.static);
|
||||
// Basic static files
|
||||
fs.copySync(polyPath("public"), paths.root);
|
||||
|
||||
copyPolyfills(staticDir);
|
||||
copyFonts(staticDir);
|
||||
copyTranslations(staticDir);
|
||||
|
||||
// Panel assets
|
||||
copyFileDir(
|
||||
npmPath("react-big-calendar/lib/css/react-big-calendar.css"),
|
||||
staticPath("panels/calendar/")
|
||||
);
|
||||
copyMapPanel(staticDir);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("compress-static", () => compressStatic(paths.static));
|
||||
|
||||
gulp.task("copy-static-demo", (done) => {
|
||||
// Copy app static files
|
||||
fs.copySync(polyPath("public"), paths.demo_root);
|
||||
// Copy demo static files
|
||||
fs.copySync(path.resolve(paths.demo_dir, "public"), paths.demo_root);
|
||||
|
||||
copyPolyfills(paths.demo_static);
|
||||
copyMapPanel(paths.demo_static);
|
||||
copyFonts(paths.demo_static);
|
||||
copyTranslations(paths.demo_static);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("copy-static-cast", (done) => {
|
||||
// Copy app static files
|
||||
fs.copySync(polyPath("public/static"), paths.cast_static);
|
||||
// Copy cast static files
|
||||
fs.copySync(path.resolve(paths.cast_dir, "public"), paths.cast_root);
|
||||
|
||||
copyMapPanel(paths.cast_static);
|
||||
copyFonts(paths.cast_static);
|
||||
copyTranslations(paths.cast_static);
|
||||
done();
|
||||
});
|
||||
136
build-scripts/gulp/gen-icons.js
Normal file
@@ -0,0 +1,136 @@
|
||||
const gulp = require("gulp");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const paths = require("../paths");
|
||||
|
||||
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
|
||||
"tooltip-account", // Map
|
||||
"cart", // Shopping List
|
||||
"hammer", // developer-tools
|
||||
];
|
||||
|
||||
// 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 pth = xml.substr(start, end);
|
||||
return `<g id="${name}">${pth}</g>`;
|
||||
}
|
||||
|
||||
// Given an iconset name and icon names, generate a polymer iconset
|
||||
function generateIconset(iconsetName, iconNames) {
|
||||
const iconDefs = Array.from(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="${iconsetName}" 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);
|
||||
if (!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(searchPath, 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(searchPath, ".js", processFile);
|
||||
mapFiles(searchPath, ".ts", processFile);
|
||||
return icons;
|
||||
}
|
||||
|
||||
function genHassIcons() {
|
||||
const iconNames = findIcons("./src", "hass");
|
||||
BUILT_IN_PANEL_ICONS.forEach((name) => iconNames.add(name));
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR);
|
||||
}
|
||||
fs.writeFileSync(HASS_OUTPUT_PATH, generateIconset("hass", iconNames));
|
||||
}
|
||||
|
||||
gulp.task("gen-icons-mdi", (done) => {
|
||||
genMDIIcons();
|
||||
done();
|
||||
});
|
||||
gulp.task("gen-icons-hass", (done) => {
|
||||
genHassIcons();
|
||||
done();
|
||||
});
|
||||
gulp.task("gen-icons", gulp.series("gen-icons-hass", "gen-icons-mdi"));
|
||||
|
||||
gulp.task("gen-icons-demo", (done) => {
|
||||
const iconNames = findIcons(path.resolve(paths.demo_dir, "./src"), "hademo");
|
||||
fs.writeFileSync(
|
||||
path.resolve(paths.demo_dir, "hademo-icons.html"),
|
||||
generateIconset("hademo", iconNames)
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
findIcons,
|
||||
generateIconset,
|
||||
genMDIIcons,
|
||||
};
|
||||
33
build-scripts/gulp/service-worker.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// Generate service worker.
|
||||
// Based on manifest, create a file with the content as service_worker.js
|
||||
/* eslint-disable import/no-dynamic-require */
|
||||
/* eslint-disable global-require */
|
||||
const gulp = require("gulp");
|
||||
const path = require("path");
|
||||
const fs = require("fs-extra");
|
||||
const config = require("../paths.js");
|
||||
|
||||
const swPath = path.resolve(config.root, "service_worker.js");
|
||||
|
||||
const writeSW = (content) => fs.outputFileSync(swPath, content.trim() + "\n");
|
||||
|
||||
gulp.task("gen-service-worker-dev", (done) => {
|
||||
writeSW(
|
||||
`
|
||||
console.debug('Service worker disabled in development');
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
`
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-service-worker-prod", (done) => {
|
||||
fs.copySync(
|
||||
path.resolve(config.output, "service_worker.js"),
|
||||
path.resolve(config.root, "service_worker.js")
|
||||
);
|
||||
done();
|
||||
});
|
||||
404
build-scripts/gulp/translations.js
Executable file
@@ -0,0 +1,404 @@
|
||||
const del = require("del");
|
||||
const path = require("path");
|
||||
const gulp = require("gulp");
|
||||
const fs = require("fs");
|
||||
const foreach = require("gulp-foreach");
|
||||
const hash = require("gulp-hash");
|
||||
const hashFilename = require("gulp-hash-filename");
|
||||
const merge = require("gulp-merge-json");
|
||||
const minify = require("gulp-jsonminify");
|
||||
const rename = require("gulp-rename");
|
||||
const transform = require("gulp-json-transform");
|
||||
|
||||
const inDir = "translations";
|
||||
const workDir = "build-translations";
|
||||
const fullDir = workDir + "/full";
|
||||
const coreDir = workDir + "/core";
|
||||
const outDir = workDir + "/output";
|
||||
|
||||
String.prototype.rsplit = function(sep, maxsplit) {
|
||||
var split = this.split(sep);
|
||||
return maxsplit
|
||||
? [split.slice(0, -maxsplit).join(sep)].concat(split.slice(-maxsplit))
|
||||
: split;
|
||||
};
|
||||
|
||||
// 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",
|
||||
"profile",
|
||||
"shopping-list",
|
||||
"page-authorize",
|
||||
"page-demo",
|
||||
"page-onboarding",
|
||||
"developer-tools",
|
||||
];
|
||||
|
||||
const tasks = [];
|
||||
|
||||
function recursiveFlatten(prefix, data) {
|
||||
let output = {};
|
||||
Object.keys(data).forEach(function(key) {
|
||||
if (typeof data[key] === "object") {
|
||||
output = Object.assign(
|
||||
{},
|
||||
output,
|
||||
recursiveFlatten(prefix + key + ".", data[key])
|
||||
);
|
||||
} else {
|
||||
output[prefix + key] = data[key];
|
||||
}
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
function flatten(data) {
|
||||
return recursiveFlatten("", data);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function recursiveEmpty(data) {
|
||||
const newData = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (data[key]) {
|
||||
if (typeof data[key] === "object") {
|
||||
newData[key] = recursiveEmpty(data[key]);
|
||||
} else {
|
||||
newData[key] = "TRANSLATED";
|
||||
}
|
||||
}
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
let taskName = "clean-translations";
|
||||
gulp.task(taskName, function() {
|
||||
return del([`${outDir}/**/*.json`]);
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
gulp.task("ensure-translations-build-dir", (done) => {
|
||||
if (!fs.existsSync(workDir)) {
|
||||
fs.mkdirSync(workDir);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
taskName = "create-test-metadata";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("ensure-translations-build-dir", function writeTestMetaData(cb) {
|
||||
fs.writeFile(
|
||||
workDir + "/testMetadata.json",
|
||||
JSON.stringify({
|
||||
test: {
|
||||
nativeName: "Test",
|
||||
},
|
||||
}),
|
||||
cb
|
||||
);
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = "create-test-translation";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("create-test-metadata", function() {
|
||||
return gulp
|
||||
.src("src/translations/en.json")
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return recursiveEmpty(data);
|
||||
})
|
||||
)
|
||||
.pipe(rename("test.json"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
taskName = "build-master-translation";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("clean-translations", function() {
|
||||
return gulp
|
||||
.src("src/translations/en.json")
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return lokalise_transform(data, data);
|
||||
})
|
||||
)
|
||||
.pipe(rename("translationMaster.json"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = "build-merged-translations";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-master-translation", function() {
|
||||
return gulp
|
||||
.src([inDir + "/*.json", workDir + "/test.json"], { allowEmpty: true })
|
||||
.pipe(
|
||||
foreach(function(stream, file) {
|
||||
// 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 = [workDir + "/translationMaster.json"];
|
||||
for (let i = 1; i <= subtags.length; i++) {
|
||||
const lang = subtags.slice(0, i).join("-");
|
||||
if (lang === "test") {
|
||||
src.push(workDir + "/test.json");
|
||||
} else if (lang !== "en") {
|
||||
src.push(inDir + "/" + lang + ".json");
|
||||
}
|
||||
}
|
||||
return gulp
|
||||
.src(src, { allowEmpty: true })
|
||||
.pipe(transform((data) => emptyFilter(data)))
|
||||
.pipe(
|
||||
merge({
|
||||
fileName: tr + ".json",
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(fullDir));
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
|
||||
const splitTasks = [];
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
taskName = "build-translation-fragment-" + fragment;
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("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,
|
||||
gulp.series("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,
|
||||
gulp.series(...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(hashFilename())
|
||||
.pipe(
|
||||
rename((filePath) => {
|
||||
if (filePath.dirname === "core") {
|
||||
filePath.dirname = "";
|
||||
}
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(outDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = "build-translation-fingerprints";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-flattened-translations", function() {
|
||||
return gulp
|
||||
.src(outDir + "/**/*.json")
|
||||
.pipe(
|
||||
rename({
|
||||
extname: "",
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
hash({
|
||||
algorithm: "md5",
|
||||
hashLength: 32,
|
||||
template: "<%= name %>.json",
|
||||
})
|
||||
)
|
||||
.pipe(hash.manifest("translationFingerprints.json"))
|
||||
.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 [path, _md5] = key.rsplit("-", 1);
|
||||
// let translation = key;
|
||||
let translation = path;
|
||||
const parts = translation.split("/");
|
||||
if (parts.length === 2) {
|
||||
translation = parts[1];
|
||||
}
|
||||
if (!(translation in newData)) {
|
||||
newData[translation] = {
|
||||
fingerprints: {},
|
||||
};
|
||||
}
|
||||
newData[translation].fingerprints[path] = value;
|
||||
});
|
||||
return newData;
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(workDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = "build-translations";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-translation-fingerprints", function() {
|
||||
return gulp
|
||||
.src(
|
||||
[
|
||||
"src/translations/translationMetadata.json",
|
||||
workDir + "/testMetadata.json",
|
||||
workDir + "/translationFingerprints.json",
|
||||
],
|
||||
{ allowEmpty: true }
|
||||
)
|
||||
.pipe(merge({}))
|
||||
.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);
|
||||
|
||||
module.exports = tasks;
|
||||
170
build-scripts/gulp/webpack.js
Normal file
@@ -0,0 +1,170 @@
|
||||
// Tasks to run webpack.
|
||||
const gulp = require("gulp");
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const WebpackDevServer = require("webpack-dev-server");
|
||||
const log = require("fancy-log");
|
||||
const paths = require("../paths");
|
||||
const {
|
||||
createAppConfig,
|
||||
createDemoConfig,
|
||||
createCastConfig,
|
||||
} = require("../webpack");
|
||||
|
||||
const handler = (done) => (err, stats) => {
|
||||
if (err) {
|
||||
console.log(err.stack || err);
|
||||
if (err.details) {
|
||||
console.log(err.details);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
log(`Build done @ ${new Date().toLocaleTimeString()}`);
|
||||
|
||||
if (stats.hasErrors() || stats.hasWarnings()) {
|
||||
console.log(stats.toString("minimal"));
|
||||
}
|
||||
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
gulp.task("webpack-watch-app", () => {
|
||||
const compiler = webpack([
|
||||
createAppConfig({
|
||||
isProdBuild: false,
|
||||
latestBuild: true,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
createAppConfig({
|
||||
isProdBuild: false,
|
||||
latestBuild: false,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
]);
|
||||
compiler.watch({}, handler());
|
||||
// we are not calling done, so this command will run forever
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"webpack-prod-app",
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
webpack(
|
||||
[
|
||||
createAppConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: true,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
createAppConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: false,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
],
|
||||
handler(resolve)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("webpack-dev-server-demo", () => {
|
||||
const compiler = webpack([
|
||||
createDemoConfig({
|
||||
isProdBuild: false,
|
||||
latestBuild: false,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
createDemoConfig({
|
||||
isProdBuild: false,
|
||||
latestBuild: true,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
]);
|
||||
|
||||
new WebpackDevServer(compiler, {
|
||||
open: true,
|
||||
watchContentBase: true,
|
||||
contentBase: path.resolve(paths.demo_dir, "dist"),
|
||||
}).listen(8090, "localhost", function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
// Server listening
|
||||
log("[webpack-dev-server]", "http://localhost:8090");
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"webpack-prod-demo",
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
webpack(
|
||||
[
|
||||
createDemoConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: false,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
createDemoConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: true,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
],
|
||||
handler(resolve)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("webpack-dev-server-cast", () => {
|
||||
const compiler = webpack([
|
||||
createCastConfig({
|
||||
isProdBuild: false,
|
||||
latestBuild: false,
|
||||
}),
|
||||
createCastConfig({
|
||||
isProdBuild: false,
|
||||
latestBuild: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
new WebpackDevServer(compiler, {
|
||||
open: true,
|
||||
watchContentBase: true,
|
||||
contentBase: path.resolve(paths.cast_dir, "dist"),
|
||||
}).listen(
|
||||
8080,
|
||||
// Accessible from the network, because that's how Cast hits it.
|
||||
"0.0.0.0",
|
||||
function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
// Server listening
|
||||
log("[webpack-dev-server]", "http://localhost:8080");
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"webpack-prod-cast",
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
webpack(
|
||||
[
|
||||
createCastConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: false,
|
||||
}),
|
||||
createCastConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: true,
|
||||
}),
|
||||
],
|
||||
handler(resolve)
|
||||
)
|
||||
)
|
||||
);
|
||||
23
build-scripts/paths.js
Normal file
@@ -0,0 +1,23 @@
|
||||
var path = require("path");
|
||||
|
||||
module.exports = {
|
||||
polymer_dir: path.resolve(__dirname, ".."),
|
||||
|
||||
build_dir: path.resolve(__dirname, "../build"),
|
||||
root: path.resolve(__dirname, "../hass_frontend"),
|
||||
static: path.resolve(__dirname, "../hass_frontend/static"),
|
||||
output: path.resolve(__dirname, "../hass_frontend/frontend_latest"),
|
||||
output_es5: path.resolve(__dirname, "../hass_frontend/frontend_es5"),
|
||||
|
||||
demo_dir: path.resolve(__dirname, "../demo"),
|
||||
demo_root: path.resolve(__dirname, "../demo/dist"),
|
||||
demo_static: path.resolve(__dirname, "../demo/dist/static"),
|
||||
demo_output: path.resolve(__dirname, "../demo/dist/frontend_latest"),
|
||||
demo_output_es5: path.resolve(__dirname, "../demo/dist/frontend_es5"),
|
||||
|
||||
cast_dir: path.resolve(__dirname, "../cast"),
|
||||
cast_root: path.resolve(__dirname, "../cast/dist"),
|
||||
cast_static: path.resolve(__dirname, "../cast/dist/static"),
|
||||
cast_output: path.resolve(__dirname, "../cast/dist/frontend_latest"),
|
||||
cast_output_es5: path.resolve(__dirname, "../cast/dist/frontend_es5"),
|
||||
};
|
||||
288
build-scripts/webpack.js
Normal file
@@ -0,0 +1,288 @@
|
||||
const webpack = require("webpack");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const WorkboxPlugin = require("workbox-webpack-plugin");
|
||||
const CompressionPlugin = require("compression-webpack-plugin");
|
||||
const zopfli = require("@gfx/zopfli");
|
||||
const ManifestPlugin = require("webpack-manifest-plugin");
|
||||
const paths = require("./paths.js");
|
||||
|
||||
let version = fs
|
||||
.readFileSync(path.resolve(paths.polymer_dir, "setup.py"), "utf8")
|
||||
.match(/\d{8}\.\d+/);
|
||||
if (!version) {
|
||||
throw Error("Version not found");
|
||||
}
|
||||
version = version[0];
|
||||
|
||||
const genMode = (isProdBuild) => (isProdBuild ? "production" : "development");
|
||||
const genDevTool = (isProdBuild) =>
|
||||
isProdBuild ? "source-map" : "inline-cheap-module-source-map";
|
||||
const genFilename = (isProdBuild, dontHash = new Set()) => ({ chunk }) => {
|
||||
if (!isProdBuild || dontHash.has(chunk.name)) {
|
||||
return `${chunk.name}.js`;
|
||||
}
|
||||
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
|
||||
};
|
||||
const genChunkFilename = (isProdBuild, isStatsBuild) =>
|
||||
isProdBuild && !isStatsBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
|
||||
|
||||
const resolve = {
|
||||
extensions: [".ts", ".js", ".json", ".tsx"],
|
||||
alias: {
|
||||
react: "preact-compat",
|
||||
"react-dom": "preact-compat",
|
||||
// Not necessary unless you consume a module using `createClass`
|
||||
"create-react-class": "preact-compat/lib/create-react-class",
|
||||
// Not necessary unless you consume a module requiring `react-dom-factories`
|
||||
"react-dom-factories": "preact-compat/lib/react-dom-factories",
|
||||
},
|
||||
};
|
||||
|
||||
const tsLoader = (latestBuild) => ({
|
||||
test: /\.ts|tsx$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
compilerOptions: latestBuild
|
||||
? { noEmit: false }
|
||||
: { target: "es5", noEmit: false },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const cssLoader = {
|
||||
test: /\.css$/,
|
||||
use: "raw-loader",
|
||||
};
|
||||
const htmlLoader = {
|
||||
test: /\.(html)$/,
|
||||
use: {
|
||||
loader: "html-loader",
|
||||
options: {
|
||||
exportAsEs6Default: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const plugins = [
|
||||
// Ignore moment.js locales
|
||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||
// Color.js is bloated, it contains all color definitions for all material color sets.
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/@polymer\/paper-styles\/color\.js$/,
|
||||
path.resolve(paths.polymer_dir, "src/util/empty.js")
|
||||
),
|
||||
// Ignore roboto pointing at CDN. We use local font-roboto-local.
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/@polymer\/font-roboto\/roboto\.js$/,
|
||||
path.resolve(paths.polymer_dir, "src/util/empty.js")
|
||||
),
|
||||
// Ignore mwc icons pointing at CDN.
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/@material\/mwc-icon\/mwc-icon-font\.js$/,
|
||||
path.resolve(paths.polymer_dir, "src/util/empty.js")
|
||||
),
|
||||
];
|
||||
|
||||
const optimization = (latestBuild) => ({
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
extractComments: true,
|
||||
sourceMap: true,
|
||||
terserOptions: {
|
||||
safari10: true,
|
||||
ecma: latestBuild ? undefined : 5,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
||||
const isCI = process.env.CI === "true";
|
||||
|
||||
// Create an object mapping browser urls to their paths during build
|
||||
const translationMetadata = require("../build-translations/translationMetadata.json");
|
||||
const workBoxTranslationsTemplatedURLs = {};
|
||||
const englishFP = translationMetadata.translations.en.fingerprints;
|
||||
Object.keys(englishFP).forEach((key) => {
|
||||
workBoxTranslationsTemplatedURLs[
|
||||
`/static/translations/${englishFP[key]}`
|
||||
] = `build-translations/output/${key}.json`;
|
||||
});
|
||||
|
||||
const entry = {
|
||||
app: "./src/entrypoints/app.ts",
|
||||
authorize: "./src/entrypoints/authorize.ts",
|
||||
onboarding: "./src/entrypoints/onboarding.ts",
|
||||
core: "./src/entrypoints/core.ts",
|
||||
compatibility: "./src/entrypoints/compatibility.ts",
|
||||
"custom-panel": "./src/entrypoints/custom-panel.ts",
|
||||
"hass-icons": "./src/entrypoints/hass-icons.ts",
|
||||
};
|
||||
|
||||
return {
|
||||
mode: genMode(isProdBuild),
|
||||
devtool: genDevTool(isProdBuild),
|
||||
entry,
|
||||
module: {
|
||||
rules: [tsLoader(latestBuild), cssLoader, htmlLoader],
|
||||
},
|
||||
optimization: optimization(latestBuild),
|
||||
plugins: [
|
||||
new ManifestPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: JSON.stringify(!isProdBuild),
|
||||
__DEMO__: false,
|
||||
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
||||
__VERSION__: JSON.stringify(version),
|
||||
__STATIC_PATH__: "/static/",
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
isProdBuild ? "production" : "development"
|
||||
),
|
||||
}),
|
||||
...plugins,
|
||||
isProdBuild &&
|
||||
!isCI &&
|
||||
!isStatsBuild &&
|
||||
new CompressionPlugin({
|
||||
cache: true,
|
||||
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
|
||||
algorithm(input, compressionOptions, callback) {
|
||||
return zopfli.gzip(input, compressionOptions, callback);
|
||||
},
|
||||
}),
|
||||
latestBuild &&
|
||||
new WorkboxPlugin.InjectManifest({
|
||||
swSrc: "./src/entrypoints/service-worker-hass.js",
|
||||
swDest: "service_worker.js",
|
||||
importWorkboxFrom: "local",
|
||||
include: [/\.js$/],
|
||||
templatedURLs: {
|
||||
...workBoxTranslationsTemplatedURLs,
|
||||
"/static/icons/favicon-192x192.png":
|
||||
"public/icons/favicon-192x192.png",
|
||||
"/static/fonts/roboto/Roboto-Light.woff2":
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2",
|
||||
"/static/fonts/roboto/Roboto-Medium.woff2":
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2",
|
||||
"/static/fonts/roboto/Roboto-Regular.woff2":
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2",
|
||||
"/static/fonts/roboto/Roboto-Bold.woff2":
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2",
|
||||
},
|
||||
}),
|
||||
].filter(Boolean),
|
||||
output: {
|
||||
filename: genFilename(isProdBuild),
|
||||
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
|
||||
path: latestBuild ? paths.output : paths.output_es5,
|
||||
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
|
||||
// For workerize loader
|
||||
globalObject: "self",
|
||||
},
|
||||
resolve,
|
||||
};
|
||||
};
|
||||
|
||||
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
||||
return {
|
||||
mode: genMode(isProdBuild),
|
||||
devtool: genDevTool(isProdBuild),
|
||||
entry: {
|
||||
main: "./demo/src/entrypoint.ts",
|
||||
compatibility: "./src/entrypoints/compatibility.ts",
|
||||
},
|
||||
module: {
|
||||
rules: [tsLoader(latestBuild), cssLoader, htmlLoader],
|
||||
},
|
||||
optimization: optimization(latestBuild),
|
||||
plugins: [
|
||||
new ManifestPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: !isProdBuild,
|
||||
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
||||
__VERSION__: JSON.stringify(`DEMO-${version}`),
|
||||
__DEMO__: true,
|
||||
__STATIC_PATH__: "/static/",
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
isProdBuild ? "production" : "development"
|
||||
),
|
||||
}),
|
||||
...plugins,
|
||||
].filter(Boolean),
|
||||
resolve,
|
||||
output: {
|
||||
filename: genFilename(isProdBuild),
|
||||
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
|
||||
path: path.resolve(
|
||||
paths.demo_root,
|
||||
latestBuild ? "frontend_latest" : "frontend_es5"
|
||||
),
|
||||
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
|
||||
// For workerize loader
|
||||
globalObject: "self",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const createCastConfig = ({ isProdBuild, latestBuild }) => {
|
||||
const isStatsBuild = false;
|
||||
const entry = {
|
||||
launcher: "./cast/src/launcher/entrypoint.ts",
|
||||
};
|
||||
|
||||
if (latestBuild) {
|
||||
entry.receiver = "./cast/src/receiver/entrypoint.ts";
|
||||
}
|
||||
|
||||
return {
|
||||
mode: genMode(isProdBuild),
|
||||
devtool: genDevTool(isProdBuild),
|
||||
entry,
|
||||
module: {
|
||||
rules: [tsLoader(latestBuild), cssLoader, htmlLoader],
|
||||
},
|
||||
optimization: optimization(latestBuild),
|
||||
plugins: [
|
||||
new ManifestPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: !isProdBuild,
|
||||
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
||||
__VERSION__: JSON.stringify(version),
|
||||
__DEMO__: false,
|
||||
__STATIC_PATH__: "/static/",
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
isProdBuild ? "production" : "development"
|
||||
),
|
||||
}),
|
||||
...plugins,
|
||||
].filter(Boolean),
|
||||
resolve,
|
||||
output: {
|
||||
filename: genFilename(isProdBuild),
|
||||
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
|
||||
path: path.resolve(
|
||||
paths.cast_root,
|
||||
latestBuild ? "frontend_latest" : "frontend_es5"
|
||||
),
|
||||
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
|
||||
// For workerize loader
|
||||
globalObject: "self",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
resolve,
|
||||
plugins,
|
||||
optimization,
|
||||
createAppConfig,
|
||||
createDemoConfig,
|
||||
createCastConfig,
|
||||
};
|
||||
56
cast/README.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Home Assistant Cast
|
||||
|
||||
Home Assistant Cast is made up of two separate applications:
|
||||
|
||||
- Chromecast receiver application that can connect to Home Assistant and display relevant information.
|
||||
- Launcher website that allows users to authorize with their Home Assistant installation and launch the receiver app on their Chromecast.
|
||||
|
||||
## Development
|
||||
|
||||
- Run `script/develop_cast` to launch the Cast receiver dev server. Keep this running.
|
||||
- Navigate to http://localhost:8080 to start the launcher
|
||||
- Debug the receiver running on the Chromecast via [chrome://inspect/#devices](chrome://inspect/#devices)
|
||||
|
||||
## Setting up development environment
|
||||
|
||||
### Registering development cast app
|
||||
|
||||
- Go to https://cast.google.com/publish and enroll your account for the Google Cast SDK (costs \$5)
|
||||
- Register your Chromecast as a testing device by entering the serial
|
||||
- Add new application -> Custom Receiver
|
||||
- Name: Home Assistant Dev
|
||||
- Receiver Application URL: http://IP-OF-DEV-MACHINE:8080/receiver.html
|
||||
- Guest Mode: off
|
||||
- Google Case for Audio: off
|
||||
|
||||
### Setting dev variables
|
||||
|
||||
Open `src/cast/dev_const.ts` and change `CAST_DEV_APP_ID` to the ID of the app you just created. And set the `CAST_DEV_HASS_URL` to the url of you development machine.
|
||||
|
||||
### Changing configuration
|
||||
|
||||
In `configuration.yaml`, configure CORS for the HTTP integration:
|
||||
|
||||
```yaml
|
||||
http:
|
||||
cors_allowed_origins:
|
||||
- https://cast.home-assistant.io
|
||||
- http://IP-OF-DEV-MACHINE:8080
|
||||
```
|
||||
|
||||
## Running development
|
||||
|
||||
```bash
|
||||
cd cast
|
||||
script/develop_cast
|
||||
```
|
||||
|
||||
The launcher application will be accessible at [http://localhost:8080](http://localhost:8080) and the receiver application will be accessible at [http://localhost:8080/receiver.html](http://localhost:8080/receiver.html) (but only works if accessed by a Chromecast).
|
||||
|
||||
### Developing cast widgets in HA ui
|
||||
|
||||
If your work involves interaction with the Cast parts from the normal Home Assistant UI, you will need to have that development script running too (`script/develop`).
|
||||
|
||||
### Developing the cast demo
|
||||
|
||||
The cast demo is triggered from the Home Assistant demo. To work on that, you will also need to run the development script for the demo (`script/develop_demo`).
|
||||
BIN
cast/public/images/arsaboo.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
BIN
cast/public/images/google-nest-hub.png
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
cast/public/images/ha-cast-icon.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
cast/public/images/melody.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
18
cast/public/manifest.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"background_color": "#FFFFFF",
|
||||
"description": "Show Home Assistant on your Chromecast or Google Assistant devices with a screen.",
|
||||
"dir": "ltr",
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/ha-cast-icon.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"lang": "en-US",
|
||||
"name": "Home Assistant Cast",
|
||||
"short_name": "HA Cast",
|
||||
"start_url": "/?homescreen=1",
|
||||
"theme_color": "#03A9F4"
|
||||
}
|
||||
3
cast/public/service_worker.js
Normal file
@@ -0,0 +1,3 @@
|
||||
self.addEventListener("fetch", function(event) {
|
||||
event.respondWith(fetch(event.request));
|
||||
});
|
||||
9
cast/script/build_cast
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
# Build the cast receiver
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
./node_modules/.bin/gulp build-cast
|
||||
9
cast/script/develop_cast
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
# Develop the cast receiver
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
./node_modules/.bin/gulp develop-cast
|
||||
5
cast/script/upload
Executable file
@@ -0,0 +1,5 @@
|
||||
# Run it twice, second time we just delete.
|
||||
aws s3 sync dist s3://cast.home-assistant.io --acl public-read
|
||||
# Don't delete as it might break open sites that need to load code splitted things.
|
||||
# aws s3 sync dist s3://cast.home-assistant.io --acl public-read --delete
|
||||
# Todo : update JS first, HTML last.
|
||||
261
cast/src/html/launcher-faq.html.template
Normal file
@@ -0,0 +1,261 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Home Assistant Cast - FAQ</title>
|
||||
<link rel="icon" href="/images/ha-cast-icon.png" type="image/png" />
|
||||
<%= renderTemplate('_style_base') %>
|
||||
<style>
|
||||
body {
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
</style>
|
||||
<meta property="fb:app_id" content="338291289691179" />
|
||||
<meta property="og:title" content="FAQ - Home Assistant Cast" />
|
||||
<meta property="og:site_name" content="Home Assistant Cast" />
|
||||
<meta property="og:url" content="https://cast.home-assistant.io/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Frequently asked questions about Home Assistant Cast."
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://cast.home-assistant.io/images/google-nest-hub.png"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@home_assistant" />
|
||||
<meta name="twitter:title" content="FAQ - Home Assistant Cast" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Frequently asked questions about Home Assistant Cast."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://cast.home-assistant.io/images/google-nest-hub.png"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<%= renderTemplate('_js_base') %>
|
||||
|
||||
<script type="module" crossorigin="use-credentials">
|
||||
import "<%= latestLauncherJS %>";
|
||||
</script>
|
||||
|
||||
<script nomodule>
|
||||
(function() {
|
||||
// // Safari 10.1 supports type=module but ignores nomodule, so we add this check.
|
||||
if (!isS101) {
|
||||
_ls("/static/polyfills/custom-elements-es5-adapter.js");
|
||||
_ls("<%= es5LauncherJS %>");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<hc-layout subtitle="FAQ">
|
||||
<style>
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
<div class="card-content">
|
||||
<p><a href="/">« Back to Home Assistant Cast</a></p>
|
||||
</div>
|
||||
|
||||
<div class="section-header">What is Home Assistant Cast?</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
Home Assistant Cast allows you to show your Home Assistant data on a
|
||||
Chromecast device and allows you to interact with Home Assistant on
|
||||
Google Assistant devices with a screen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
What are the Home Assistant Cast requirements?
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
Home Assistant Cast requires a Home Assistant installation that is
|
||||
accessible via HTTPS (the url starts with "https://").
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section-header">What is Home Assistant?</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
Home Assistant is worlds biggest open source home automation platform
|
||||
with a focus on privacy and local control. You can install Home
|
||||
Assistant for free.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://www.home-assistant.io" target="_blank"
|
||||
>Visit the Home Assistant website.</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section-header" id="https">
|
||||
Why does my Home Assistant needs to be served using HTTPS?
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
The Chromecast only works with websites served over HTTPS. This means
|
||||
that the Home Assistant Cast app that runs on your Chromecast is
|
||||
served over HTTPS. Websites served over HTTPS are restricted on what
|
||||
content can be accessed on websites served over HTTP. This is called
|
||||
mixed active content (<a
|
||||
href="https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content#Mixed_active_content"
|
||||
target="_blank"
|
||||
>learn more @ MDN</a
|
||||
>).
|
||||
</p>
|
||||
<p>
|
||||
The easiest way to get your Home Assistant installation served over
|
||||
HTTPS is by signing up for
|
||||
<a href="https://www.nabucasa.com" target="_blank"
|
||||
>Home Assistant Cloud by Nabu Casa</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section-header" id="https">
|
||||
Why does Home Assistant Cast require me to authorize my Home Assistant
|
||||
instance?
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
You're currently looking at the Home Assistant Cast launcher
|
||||
application. This is a standalone application to launch Home Assistant
|
||||
Cast on your Chromecast. Because Chromecasts do not allow us to log in
|
||||
to Home Assistant, we need to supply authentication to it from the
|
||||
launcher. This authentication is obtained when you authorize your
|
||||
instance. Your authentication credentials will remain in your browser
|
||||
and on your Cast device.
|
||||
</p>
|
||||
<p>
|
||||
Your authentication credentials or Home Assistant url are never sent
|
||||
to the Cloud. You can validate this behavior in
|
||||
<a
|
||||
href="https://github.com/home-assistant/home-assistant-polymer/tree/dev/cast"
|
||||
target="_blank"
|
||||
>the source code</a
|
||||
>.
|
||||
</p>
|
||||
<p>
|
||||
The launcher application exists to make it possible to use Home
|
||||
Assistant Cast with older versions of Home Assistant.
|
||||
</p>
|
||||
<p>
|
||||
Starting with Home Assistant 0.97, Home Assistant Cast is also built
|
||||
into the Lovelace UI as a special entities card row. Since the
|
||||
Lovelace UI already has authentication, you will be able to start
|
||||
casting right away.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section-header">Wat does Home Assistant Cast do?</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
Home Assistant Cast is a receiver application for the Chromecast. When
|
||||
loaded, it will make a direct connection to your Home Assistant
|
||||
instance.
|
||||
</p>
|
||||
<p>
|
||||
Home Assistant Cast is able to render any of your Lovelace views on
|
||||
your Chromecast. Things that work in Lovelace in Home Assistant will
|
||||
work in Home Assistant Cast:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Render Lovelace views, including custom cards</li>
|
||||
<li>
|
||||
Real-time data stream will ensure the UI always shows the latest
|
||||
state of your house
|
||||
</li>
|
||||
<li>Navigate between views using navigate actions or weblinks</li>
|
||||
<li>
|
||||
Instant updates of the casted Lovelace UI when you update your
|
||||
Lovelace configuration.
|
||||
</li>
|
||||
</ul>
|
||||
<p>Things that currently do not work:</p>
|
||||
<ul>
|
||||
<li>
|
||||
Live videostreams using the streaming integration
|
||||
</li>
|
||||
<li>Specifying a view with a single card with "panel: true".</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section-header" id="https">
|
||||
How do I change what is shown on my Chromecast?
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
Home Assistant Cast allows you to show your Lovelace view on your
|
||||
Chromecast. So to edit what is shown, you need to edit your Lovelace
|
||||
UI.
|
||||
</p>
|
||||
<p>
|
||||
To edit your Lovelace UI, open Home Assistant, click on the three-dot
|
||||
menu in the top right and click on "Configure UI".
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section-header" id="browser">
|
||||
What browsers are supported?
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
Chromecast is a technology developed by Google, and is available on:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Google Chrome (all platforms except on iOS)</li>
|
||||
<li>
|
||||
Microsoft Edge (all platforms,
|
||||
<a href="https://www.microsoftedgeinsider.com" target="_blank"
|
||||
>dev and canary builds only</a
|
||||
>)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section-header">Why do some custom cards not work?</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
Home Assistant needs to be configured to allow Home Assistant Cast to
|
||||
load custom cards. Starting with Home Assistant 0.97, this is done
|
||||
automatically. If you are on an older version, or have manually
|
||||
configured CORS for the HTTP integration, add the following to your
|
||||
configuration.yaml file:
|
||||
</p>
|
||||
<pre>
|
||||
http:
|
||||
cors_allowed_origins:
|
||||
- https://cast.home-assistant.io</pre
|
||||
>
|
||||
<p>
|
||||
Some custom cards rely on things that are only available in the normal
|
||||
Home Assistant interface. This requires an update by the custom card
|
||||
developer.
|
||||
</p>
|
||||
<p>
|
||||
If you're a custom card developer: the most common mistake is that
|
||||
LitElement is extracted from an element that is not available on the
|
||||
page.
|
||||
</p>
|
||||
</div>
|
||||
</hc-layout>
|
||||
|
||||
<script>
|
||||
var _gaq = [["_setAccount", "UA-57927901-9"], ["_trackPageview"]];
|
||||
(function(d, t) {
|
||||
var g = d.createElement(t),
|
||||
s = d.getElementsByTagName(t)[0];
|
||||
g.src =
|
||||
("https:" == location.protocol ? "//ssl" : "//www") +
|
||||
".google-analytics.com/ga.js";
|
||||
s.parentNode.insertBefore(g, s);
|
||||
})(document, "script");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
54
cast/src/html/launcher.html.template
Normal file
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Home Assistant Cast</title>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="icon" href="/images/ha-cast-icon.png" type="image/png" />
|
||||
<%= renderTemplate('_style_base') %>
|
||||
<style>
|
||||
body {
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
</style>
|
||||
<meta property="fb:app_id" content="338291289691179">
|
||||
<meta property="og:title" content="Home Assistant Cast">
|
||||
<meta property="og:site_name" content="Home Assistant Cast">
|
||||
<meta property="og:url" content="https://cast.home-assistant.io/">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:description" content="Show Home Assistant on your Chromecast or Google Assistant devices with a screen.">
|
||||
<meta property="og:image" content="https://cast.home-assistant.io/images/google-nest-hub.png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@home_assistant">
|
||||
<meta name="twitter:title" content="Home Assistant Cast">
|
||||
<meta name="twitter:description" content="Show Home Assistant on your Chromecast or Google Assistant devices with a screen.">
|
||||
<meta name="twitter:image" content="https://cast.home-assistant.io/images/google-nest-hub.png">
|
||||
</head>
|
||||
<body>
|
||||
<%= renderTemplate('_js_base') %>
|
||||
|
||||
<hc-connect></hc-connect>
|
||||
|
||||
<script type="module" crossorigin="use-credentials">
|
||||
import "<%= latestLauncherJS %>";
|
||||
</script>
|
||||
|
||||
<script nomodule>
|
||||
(function() {
|
||||
// // Safari 10.1 supports type=module but ignores nomodule, so we add this check.
|
||||
if (!isS101) {
|
||||
_ls("/static/polyfills/custom-elements-es5-adapter.js");
|
||||
_ls("<%= es5LauncherJS %>");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-57927901-9', 'auto');
|
||||
ga('send', 'pageview', location.pathname.includes("auth_callback") === -1 ? location.pathname : "/");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
18
cast/src/html/receiver.html.template
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<script src="//www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script>
|
||||
<script type="module" src="<%= latestReceiverJS %>"></script>
|
||||
<%= renderTemplate('_style_base') %>
|
||||
<style>
|
||||
body {
|
||||
background-color: white;
|
||||
font-size: initial;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var _gaq=[['_setAccount','UA-57927901-10'],['_trackPageview']];
|
||||
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
|
||||
g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
|
||||
s.parentNode.insertBefore(g,s)}(document,'script'));
|
||||
</script>
|
||||
</html>
|
||||
5
cast/src/launcher/entrypoint.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import "../../../src/resources/ha-style";
|
||||
import "../../../src/resources/roboto";
|
||||
import "../../../src/components/ha-iconset-svg";
|
||||
import "../../../src/resources/hass-icons";
|
||||
import "./layout/hc-connect";
|
||||
282
cast/src/launcher/layout/hc-cast.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
import {
|
||||
customElement,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
html,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import { Connection, Auth } from "home-assistant-js-websocket";
|
||||
import "@polymer/iron-icon";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "../../../../src/components/ha-icon";
|
||||
import {
|
||||
enableWrite,
|
||||
askWrite,
|
||||
saveTokens,
|
||||
} from "../../../../src/common/auth/token_storage";
|
||||
import {
|
||||
ensureConnectedCastSession,
|
||||
castSendShowLovelaceView,
|
||||
} from "../../../../src/cast/receiver_messages";
|
||||
import "../../../../src/layouts/loading-screen";
|
||||
import { CastManager } from "../../../../src/cast/cast_manager";
|
||||
import {
|
||||
LovelaceConfig,
|
||||
getLovelaceCollection,
|
||||
} from "../../../../src/data/lovelace";
|
||||
import "./hc-layout";
|
||||
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
|
||||
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
|
||||
|
||||
@customElement("hc-cast")
|
||||
class HcCast extends LitElement {
|
||||
@property() public auth!: Auth;
|
||||
@property() public connection!: Connection;
|
||||
@property() public castManager!: CastManager;
|
||||
@property() private askWrite = false;
|
||||
@property() private lovelaceConfig?: LovelaceConfig | null;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (this.lovelaceConfig === undefined) {
|
||||
return html`
|
||||
<loading-screen></loading-screen>>
|
||||
`;
|
||||
}
|
||||
|
||||
const error =
|
||||
this.castManager.castState === "NO_DEVICES_AVAILABLE"
|
||||
? html`
|
||||
<p>
|
||||
There were no suitable Chromecast devices to cast to found.
|
||||
</p>
|
||||
`
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<hc-layout .auth=${this.auth} .connection=${this.connection}>
|
||||
${this.askWrite
|
||||
? html`
|
||||
<p class="question action-item">
|
||||
Stay logged in?
|
||||
<span>
|
||||
<mwc-button @click=${this._handleSaveTokens}>
|
||||
YES
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._handleSkipSaveTokens}>
|
||||
NO
|
||||
</mwc-button>
|
||||
</span>
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
${error
|
||||
? html`
|
||||
<div class="card-content">${error}</div>
|
||||
`
|
||||
: !this.castManager.status
|
||||
? html`
|
||||
<p class="center-item">
|
||||
<mwc-button raised @click=${this._handleLaunch}>
|
||||
<iron-icon icon="hass:cast"></iron-icon>
|
||||
Start Casting
|
||||
</mwc-button>
|
||||
</p>
|
||||
`
|
||||
: html`
|
||||
<div class="section-header">PICK A VIEW</div>
|
||||
<paper-listbox
|
||||
attr-for-selected="data-path"
|
||||
.selected=${this.castManager.status.lovelacePath || ""}
|
||||
>
|
||||
${(this.lovelaceConfig
|
||||
? this.lovelaceConfig.views
|
||||
: [generateDefaultViewConfig([], [], [], {}, () => "")]
|
||||
).map(
|
||||
(view, idx) => html`
|
||||
<paper-icon-item
|
||||
@click=${this._handlePickView}
|
||||
data-path=${view.path || idx}
|
||||
>
|
||||
${view.icon
|
||||
? html`
|
||||
<ha-icon
|
||||
.icon=${view.icon}
|
||||
slot="item-icon"
|
||||
></ha-icon>
|
||||
`
|
||||
: ""}
|
||||
${view.title || view.path}
|
||||
</paper-icon-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
`}
|
||||
<div class="card-actions">
|
||||
${this.castManager.status
|
||||
? html`
|
||||
<mwc-button @click=${this._handleLaunch}>
|
||||
<iron-icon icon="hass:cast-connected"></iron-icon>
|
||||
Manage
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
<div class="spacer"></div>
|
||||
<mwc-button @click=${this._handleLogout}>Log out</mwc-button>
|
||||
</div>
|
||||
</hc-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
const llColl = getLovelaceCollection(this.connection);
|
||||
// We first do a single refresh because we need to check if there is LL
|
||||
// configuration.
|
||||
llColl.refresh().then(
|
||||
() => {
|
||||
llColl.subscribe((config) => {
|
||||
this.lovelaceConfig = config;
|
||||
});
|
||||
},
|
||||
async () => {
|
||||
this.lovelaceConfig = null;
|
||||
}
|
||||
);
|
||||
|
||||
this.askWrite = askWrite();
|
||||
|
||||
this.castManager.addEventListener("state-changed", () => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
this.castManager.addEventListener("connection-changed", () => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
toggleAttribute(
|
||||
this,
|
||||
"hide-icons",
|
||||
this.lovelaceConfig
|
||||
? !this.lovelaceConfig.views.some((view) => view.icon)
|
||||
: true
|
||||
);
|
||||
}
|
||||
|
||||
private async _handleSkipSaveTokens() {
|
||||
this.askWrite = false;
|
||||
}
|
||||
|
||||
private async _handleSaveTokens() {
|
||||
enableWrite();
|
||||
this.askWrite = false;
|
||||
}
|
||||
|
||||
private _handleLaunch() {
|
||||
this.castManager.requestSession();
|
||||
}
|
||||
|
||||
private async _handlePickView(ev: Event) {
|
||||
const path = (ev.currentTarget as any).getAttribute("data-path");
|
||||
await ensureConnectedCastSession(this.castManager!, this.auth!);
|
||||
castSendShowLovelaceView(this.castManager, path);
|
||||
}
|
||||
|
||||
private async _handleLogout() {
|
||||
try {
|
||||
await this.auth.revoke();
|
||||
saveTokens(null);
|
||||
if (this.castManager.castSession) {
|
||||
this.castManager.castContext.endCurrentSession(true);
|
||||
}
|
||||
this.connection.close();
|
||||
location.reload();
|
||||
} catch (err) {
|
||||
alert("Unable to log out!");
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.center-item {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.question {
|
||||
position: relative;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.question:before {
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
background-color: var(--primary-color);
|
||||
opacity: 0.12;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
.connection,
|
||||
.connection a {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
mwc-button iron-icon {
|
||||
margin-right: 8px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
paper-listbox {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
paper-listbox ha-icon {
|
||||
padding: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
paper-icon-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
paper-icon-item[disabled] {
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
:host([hide-icons]) paper-icon-item {
|
||||
--paper-item-icon-width: 0px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-content a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hc-cast": HcCast;
|
||||
}
|
||||
}
|
||||
333
cast/src/launcher/layout/hc-connect.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
import {
|
||||
LitElement,
|
||||
customElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
html,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import {
|
||||
getAuth,
|
||||
createConnection,
|
||||
Auth,
|
||||
getAuthOptions,
|
||||
ERR_HASS_HOST_REQUIRED,
|
||||
ERR_INVALID_HTTPS_TO_HTTP,
|
||||
Connection,
|
||||
ERR_CANNOT_CONNECT,
|
||||
ERR_INVALID_AUTH,
|
||||
} from "home-assistant-js-websocket";
|
||||
import "@polymer/iron-icon";
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
loadTokens,
|
||||
saveTokens,
|
||||
} from "../../../../src/common/auth/token_storage";
|
||||
import "../../../../src/layouts/loading-screen";
|
||||
import { CastManager, getCastManager } from "../../../../src/cast/cast_manager";
|
||||
import "./hc-layout";
|
||||
import { castSendShowDemo } from "../../../../src/cast/receiver_messages";
|
||||
import { registerServiceWorker } from "../../../../src/util/register-service-worker";
|
||||
|
||||
const seeFAQ = (qid) => html`
|
||||
See <a href="./faq.html${qid ? `#${qid}` : ""}">the FAQ</a> for more
|
||||
information.
|
||||
`;
|
||||
const translateErr = (err) =>
|
||||
err === ERR_CANNOT_CONNECT
|
||||
? "Unable to connect"
|
||||
: err === ERR_HASS_HOST_REQUIRED
|
||||
? "Please enter a Home Assistant URL."
|
||||
: err === ERR_INVALID_HTTPS_TO_HTTP
|
||||
? html`
|
||||
Cannot connect to Home Assistant instances over "http://".
|
||||
${seeFAQ("https")}
|
||||
`
|
||||
: `Unknown error (${err}).`;
|
||||
|
||||
const INTRO = html`
|
||||
<p>
|
||||
Home Assistant Cast allows you to cast your Home Assistant installation to
|
||||
Chromecast video devices and to Google Assistant devices with a screen.
|
||||
</p>
|
||||
<p>
|
||||
For more information, see the
|
||||
<a href="./faq.html">frequently asked questions</a>.
|
||||
</p>
|
||||
`;
|
||||
|
||||
@customElement("hc-connect")
|
||||
export class HcConnect extends LitElement {
|
||||
@property() private loading = false;
|
||||
// If we had stored credentials but we cannot connect,
|
||||
// show a screen asking retry or logout.
|
||||
@property() private cannotConnect = false;
|
||||
@property() private error?: string | TemplateResult;
|
||||
@property() private auth?: Auth;
|
||||
@property() private connection?: Connection;
|
||||
@property() private castManager?: CastManager | null;
|
||||
private openDemo = false;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (this.cannotConnect) {
|
||||
const tokens = loadTokens();
|
||||
return html`
|
||||
<hc-layout>
|
||||
<div class="card-content">
|
||||
Unable to connect to ${tokens!.hassUrl}.
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a href="/">
|
||||
<mwc-button>
|
||||
Retry
|
||||
</mwc-button>
|
||||
</a>
|
||||
<div class="spacer"></div>
|
||||
<mwc-button @click=${this._handleLogout}>Log out</mwc-button>
|
||||
</div>
|
||||
</hc-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.castManager === undefined || this.loading) {
|
||||
return html`
|
||||
<loading-screen></loading-screen>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.castManager === null) {
|
||||
return html`
|
||||
<hc-layout>
|
||||
<div class="card-content">
|
||||
${INTRO}
|
||||
<p class="error">
|
||||
The Cast API is not available in your browser.
|
||||
${seeFAQ("browser")}
|
||||
</p>
|
||||
</div>
|
||||
</hc-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!this.auth) {
|
||||
return html`
|
||||
<hc-layout>
|
||||
<div class="card-content">
|
||||
${INTRO}
|
||||
<p>
|
||||
To get started, enter your Home Assistant URL and click authorize.
|
||||
If you want a preview instead, click the show demo button.
|
||||
</p>
|
||||
<p>
|
||||
<paper-input
|
||||
label="Home Assistant URL"
|
||||
placeholder="https://abcdefghijklmnop.ui.nabu.casa"
|
||||
@keydown=${this._handleInputKeyDown}
|
||||
></paper-input>
|
||||
</p>
|
||||
${this.error
|
||||
? html`
|
||||
<p class="error">${this.error}</p>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._handleDemo}>
|
||||
Show Demo
|
||||
<iron-icon
|
||||
.icon=${this.castManager.castState === "CONNECTED"
|
||||
? "hass:cast-connected"
|
||||
: "hass:cast"}
|
||||
></iron-icon>
|
||||
</mwc-button>
|
||||
<div class="spacer"></div>
|
||||
<mwc-button @click=${this._handleConnect}>Authorize</mwc-button>
|
||||
</div>
|
||||
</hc-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hc-cast
|
||||
.connection=${this.connection}
|
||||
.auth=${this.auth}
|
||||
.castManager=${this.castManager}
|
||||
></hc-cast>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
import("./hc-cast");
|
||||
|
||||
getCastManager().then(
|
||||
async (mgr) => {
|
||||
this.castManager = mgr;
|
||||
mgr.addEventListener("connection-changed", () => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
mgr.addEventListener("state-changed", () => {
|
||||
if (this.openDemo && mgr.castState === "CONNECTED" && !this.auth) {
|
||||
castSendShowDemo(mgr);
|
||||
}
|
||||
});
|
||||
|
||||
if (location.search.indexOf("auth_callback=1") !== -1) {
|
||||
this._tryConnection("auth-callback");
|
||||
} else if (loadTokens()) {
|
||||
this._tryConnection("saved-tokens");
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.castManager = null;
|
||||
}
|
||||
);
|
||||
registerServiceWorker(false);
|
||||
}
|
||||
|
||||
private async _handleDemo() {
|
||||
this.openDemo = true;
|
||||
if (this.castManager!.status && !this.castManager!.status.showDemo) {
|
||||
castSendShowDemo(this.castManager!);
|
||||
} else {
|
||||
this.castManager!.requestSession();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleInputKeyDown(ev: KeyboardEvent) {
|
||||
// Handle pressing enter.
|
||||
if (ev.keyCode === 13) {
|
||||
this._handleConnect();
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleConnect() {
|
||||
const inputEl = this.shadowRoot!.querySelector("paper-input")!;
|
||||
const value = inputEl.value || "";
|
||||
this.error = undefined;
|
||||
|
||||
if (value === "") {
|
||||
this.error = "Please enter a Home Assistant URL.";
|
||||
return;
|
||||
} else if (value.indexOf("://") === -1) {
|
||||
this.error =
|
||||
"Please enter your full URL, including the protocol part (https://).";
|
||||
return;
|
||||
}
|
||||
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch (err) {
|
||||
this.error = "Invalid URL";
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.protocol === "http:" && url.hostname !== "localhost") {
|
||||
this.error = translateErr(ERR_INVALID_HTTPS_TO_HTTP);
|
||||
return;
|
||||
}
|
||||
await this._tryConnection("user-request", `${url.protocol}//${url.host}`);
|
||||
}
|
||||
|
||||
private async _tryConnection(
|
||||
init: "auth-callback" | "user-request" | "saved-tokens",
|
||||
hassUrl?: string
|
||||
) {
|
||||
const options: getAuthOptions = {
|
||||
saveTokens,
|
||||
loadTokens: () => Promise.resolve(loadTokens()),
|
||||
};
|
||||
if (hassUrl) {
|
||||
options.hassUrl = hassUrl;
|
||||
}
|
||||
let auth: Auth;
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
auth = await getAuth(options);
|
||||
} catch (err) {
|
||||
if (init === "saved-tokens" && err === ERR_CANNOT_CONNECT) {
|
||||
this.cannotConnect = true;
|
||||
return;
|
||||
}
|
||||
this.error = translateErr(err);
|
||||
this.loading = false;
|
||||
return;
|
||||
} finally {
|
||||
// Clear url if we have a auth callback in url.
|
||||
if (location.search.includes("auth_callback=1")) {
|
||||
history.replaceState(null, "", location.pathname);
|
||||
}
|
||||
}
|
||||
|
||||
let conn: Connection;
|
||||
|
||||
try {
|
||||
conn = await createConnection({ auth });
|
||||
} catch (err) {
|
||||
// In case of saved tokens, silently solve problems.
|
||||
if (init === "saved-tokens") {
|
||||
if (err === ERR_CANNOT_CONNECT) {
|
||||
this.cannotConnect = true;
|
||||
} else if (err === ERR_INVALID_AUTH) {
|
||||
saveTokens(null);
|
||||
}
|
||||
} else {
|
||||
this.error = translateErr(err);
|
||||
}
|
||||
|
||||
return;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
this.auth = auth;
|
||||
this.connection = conn;
|
||||
this.castManager!.auth = auth;
|
||||
}
|
||||
|
||||
private async _handleLogout() {
|
||||
try {
|
||||
saveTokens(null);
|
||||
location.reload();
|
||||
} catch (err) {
|
||||
alert("Unable to log out!");
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.card-content a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.card-actions a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error a {
|
||||
color: darkred;
|
||||
}
|
||||
|
||||
mwc-button iron-icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hc-connect": HcConnect;
|
||||
}
|
||||
}
|
||||
166
cast/src/launcher/layout/hc-layout.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import {
|
||||
customElement,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
CSSResult,
|
||||
css,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import {
|
||||
Auth,
|
||||
Connection,
|
||||
HassUser,
|
||||
getUser,
|
||||
} from "home-assistant-js-websocket";
|
||||
import "../../../../src/components/ha-card";
|
||||
|
||||
@customElement("hc-layout")
|
||||
class HcLayout extends LitElement {
|
||||
@property() public subtitle?: string | undefined;
|
||||
@property() public auth?: Auth;
|
||||
@property() public connection?: Connection;
|
||||
@property() public user?: HassUser;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="layout">
|
||||
<img class="hero" src="/images/google-nest-hub.png" />
|
||||
<div class="card-header">
|
||||
Home Assistant Cast${this.subtitle ? ` – ${this.subtitle}` : ""}
|
||||
${this.auth
|
||||
? html`
|
||||
<div class="subtitle">
|
||||
<a href=${this.auth.data.hassUrl} target="_blank"
|
||||
>${this.auth.data.hassUrl.substr(
|
||||
this.auth.data.hassUrl.indexOf("//") + 2
|
||||
)}</a
|
||||
>
|
||||
${this.user
|
||||
? html`
|
||||
– ${this.user.name}
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</ha-card>
|
||||
<div class="footer">
|
||||
<a href="./faq.html">Frequently Asked Questions</a> – Found a bug? Let
|
||||
@balloob know
|
||||
<!-- <a
|
||||
href="https://github.com/home-assistant/home-assistant-polymer/issues"
|
||||
target="_blank"
|
||||
>Let us know!</a
|
||||
> -->
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (this.connection) {
|
||||
getUser(this.connection).then((user) => {
|
||||
this.user = user;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
min-height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
color: var(--ha-card-header-color, --primary-text-color);
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: var(--ha-card-header-font-size, 24px);
|
||||
letter-spacing: -0.012em;
|
||||
line-height: 32px;
|
||||
padding: 24px 16px 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--secondary-text-color);
|
||||
line-height: initial;
|
||||
}
|
||||
.subtitle a {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
:host ::slotted(.card-content:not(:first-child)),
|
||||
slot:not(:first-child)::slotted(.card-content) {
|
||||
padding-top: 0px;
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
:host ::slotted(.section-header) {
|
||||
font-weight: 500;
|
||||
padding: 4px 16px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
:host ::slotted(.card-content) {
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:host ::slotted(.card-actions) {
|
||||
border-top: 1px solid #e8e8e8;
|
||||
padding: 5px 16px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
padding: 8px 0 24px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.footer a {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
:host {
|
||||
justify-content: flex-start;
|
||||
min-height: 90%;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hc-layout": HcLayout;
|
||||
}
|
||||
}
|
||||
1
cast/src/receiver/cast_context.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const castContext = cast.framework.CastReceiverContext.getInstance();
|
||||
141
cast/src/receiver/demo/cast-demo-entities.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { Entity, convertEntities } from "../../../../src/fake_data/entity";
|
||||
|
||||
export const castDemoEntities: () => Entity[] = () =>
|
||||
convertEntities({
|
||||
"light.reading_light": {
|
||||
entity_id: "light.reading_light",
|
||||
state: "on",
|
||||
attributes: {
|
||||
friendly_name: "Reading Light",
|
||||
},
|
||||
},
|
||||
"light.ceiling": {
|
||||
entity_id: "light.ceiling",
|
||||
state: "on",
|
||||
attributes: {
|
||||
friendly_name: "Ceiling lights",
|
||||
},
|
||||
},
|
||||
"light.standing_lamp": {
|
||||
entity_id: "light.standing_lamp",
|
||||
state: "off",
|
||||
attributes: {
|
||||
friendly_name: "Standing Lamp",
|
||||
},
|
||||
},
|
||||
"sensor.temperature_inside": {
|
||||
entity_id: "sensor.temperature_inside",
|
||||
state: "22.7",
|
||||
attributes: {
|
||||
battery_level: 78,
|
||||
unit_of_measurement: "\u00b0C",
|
||||
friendly_name: "Inside",
|
||||
device_class: "temperature",
|
||||
},
|
||||
},
|
||||
"sensor.temperature_outside": {
|
||||
entity_id: "sensor.temperature_outside",
|
||||
state: "31.4",
|
||||
attributes: {
|
||||
battery_level: 53,
|
||||
unit_of_measurement: "\u00b0C",
|
||||
friendly_name: "Outside",
|
||||
device_class: "temperature",
|
||||
},
|
||||
},
|
||||
"person.arsaboo": {
|
||||
entity_id: "person.arsaboo",
|
||||
state: "not_home",
|
||||
attributes: {
|
||||
radius: 50,
|
||||
friendly_name: "Arsaboo",
|
||||
latitude: 52.3579946,
|
||||
longitude: 4.8664597,
|
||||
entity_picture: "/images/arsaboo.jpg",
|
||||
},
|
||||
},
|
||||
"person.melody": {
|
||||
entity_id: "person.melody",
|
||||
state: "not_home",
|
||||
attributes: {
|
||||
radius: 50,
|
||||
friendly_name: "Melody",
|
||||
latitude: 52.3408927,
|
||||
longitude: 4.8711073,
|
||||
entity_picture: "/images/melody.jpg",
|
||||
},
|
||||
},
|
||||
"zone.home": {
|
||||
entity_id: "zone.home",
|
||||
state: "zoning",
|
||||
attributes: {
|
||||
hidden: true,
|
||||
latitude: 52.3631339,
|
||||
longitude: 4.8903147,
|
||||
radius: 100,
|
||||
friendly_name: "Home",
|
||||
icon: "hass:home",
|
||||
},
|
||||
},
|
||||
"input_number.harmonyvolume": {
|
||||
entity_id: "input_number.harmonyvolume",
|
||||
state: "18.0",
|
||||
attributes: {
|
||||
initial: 30,
|
||||
min: 1,
|
||||
max: 100,
|
||||
step: 1,
|
||||
mode: "slider",
|
||||
friendly_name: "Volume",
|
||||
icon: "hass:volume-high",
|
||||
},
|
||||
},
|
||||
"climate.upstairs": {
|
||||
entity_id: "climate.upstairs",
|
||||
state: "auto",
|
||||
attributes: {
|
||||
current_temperature: 24,
|
||||
min_temp: 15,
|
||||
max_temp: 30,
|
||||
temperature: null,
|
||||
target_temp_high: 26,
|
||||
target_temp_low: 18,
|
||||
fan_mode: "auto",
|
||||
fan_modes: ["auto", "on"],
|
||||
hvac_modes: ["auto", "cool", "heat", "off"],
|
||||
aux_heat: "off",
|
||||
actual_humidity: 30,
|
||||
fan: "on",
|
||||
operation: "fan",
|
||||
fan_min_on_time: 10,
|
||||
friendly_name: "Upstairs",
|
||||
supported_features: 27,
|
||||
preset_mode: "away",
|
||||
preset_modes: ["home", "away", "eco", "sleep"],
|
||||
},
|
||||
},
|
||||
"climate.downstairs": {
|
||||
entity_id: "climate.downstairs",
|
||||
state: "auto",
|
||||
attributes: {
|
||||
current_temperature: 22,
|
||||
min_temp: 15,
|
||||
max_temp: 30,
|
||||
temperature: null,
|
||||
target_temp_high: 24,
|
||||
target_temp_low: 20,
|
||||
fan_mode: "auto",
|
||||
fan_modes: ["auto", "on"],
|
||||
hvac_modes: ["auto", "cool", "heat", "off"],
|
||||
aux_heat: "off",
|
||||
actual_humidity: 30,
|
||||
fan: "on",
|
||||
operation: "fan",
|
||||
fan_min_on_time: 10,
|
||||
friendly_name: "Downstairs",
|
||||
supported_features: 27,
|
||||
preset_mode: "home",
|
||||
preset_modes: ["home", "away", "eco", "sleep"],
|
||||
},
|
||||
},
|
||||
});
|
||||
93
cast/src/receiver/demo/cast-demo-lovelace.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
LovelaceConfig,
|
||||
LovelaceCardConfig,
|
||||
} from "../../../../src/data/lovelace";
|
||||
import { castContext } from "../cast_context";
|
||||
|
||||
export const castDemoLovelace: () => LovelaceConfig = () => {
|
||||
const touchSupported = castContext.getDeviceCapabilities()
|
||||
.touch_input_supported;
|
||||
return {
|
||||
views: [
|
||||
{
|
||||
path: "overview",
|
||||
cards: [
|
||||
{
|
||||
type: "markdown",
|
||||
title: "Home Assistant Cast",
|
||||
content: `With Home Assistant you can easily create interfaces (just like this one) which can be shown on Chromecast devices connected to TVs or Google Assistant devices with a screen.${
|
||||
touchSupported
|
||||
? "\n\nYou are able to interact with this demo using the touch screen."
|
||||
: "\n\nOn a Google Nest Hub you are able to interact with Home Assistant Cast via the touch screen."
|
||||
}`,
|
||||
},
|
||||
{
|
||||
type: touchSupported ? "entities" : "glance",
|
||||
title: "Living Room",
|
||||
entities: [
|
||||
"light.reading_light",
|
||||
"light.ceiling",
|
||||
"light.standing_lamp",
|
||||
"input_number.harmonyvolume",
|
||||
],
|
||||
},
|
||||
{
|
||||
cards: [
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
entity: "sensor.temperature_inside",
|
||||
},
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
entity: "sensor.temperature_outside",
|
||||
},
|
||||
],
|
||||
type: "horizontal-stack",
|
||||
},
|
||||
{
|
||||
type: "map",
|
||||
entities: ["person.arsaboo", "person.melody", "zone.home"],
|
||||
aspect_ratio: touchSupported ? "16:9.3" : "16:11",
|
||||
},
|
||||
touchSupported && {
|
||||
type: "entities",
|
||||
entities: [
|
||||
{
|
||||
type: "weblink",
|
||||
url: "/lovelace/climate",
|
||||
name: "Climate controls",
|
||||
icon: "hass:arrow-right",
|
||||
},
|
||||
],
|
||||
},
|
||||
].filter(Boolean) as LovelaceCardConfig[],
|
||||
},
|
||||
{
|
||||
path: "climate",
|
||||
cards: [
|
||||
{
|
||||
type: "thermostat",
|
||||
entity: "climate.downstairs",
|
||||
},
|
||||
{
|
||||
type: "entities",
|
||||
entities: [
|
||||
{
|
||||
type: "weblink",
|
||||
url: "/lovelace/overview",
|
||||
name: "Back",
|
||||
icon: "hass:arrow-left",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "thermostat",
|
||||
entity: "climate.upstairs",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
42
cast/src/receiver/entrypoint.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import "../../../src/resources/custom-card-support";
|
||||
import { castContext } from "./cast_context";
|
||||
import { ReceivedMessage } from "./types";
|
||||
import { HassMessage } from "../../../src/cast/receiver_messages";
|
||||
import { HcMain } from "./layout/hc-main";
|
||||
import { CAST_NS } from "../../../src/cast/const";
|
||||
|
||||
const controller = new HcMain();
|
||||
document.body.append(controller);
|
||||
|
||||
const options = new cast.framework.CastReceiverOptions();
|
||||
options.disableIdleTimeout = true;
|
||||
options.customNamespaces = {
|
||||
// @ts-ignore
|
||||
[CAST_NS]: cast.framework.system.MessageType.JSON,
|
||||
};
|
||||
|
||||
// The docs say we need to set options.touchScreenOptimizeApp = true
|
||||
// https://developers.google.com/cast/docs/caf_receiver/customize_ui#accessing_ui_controls
|
||||
// This doesn't work.
|
||||
// @ts-ignore
|
||||
options.touchScreenOptimizedApp = true;
|
||||
|
||||
// The class reference say we can set a uiConfig in options to set it
|
||||
// https://developers.google.com/cast/docs/reference/caf_receiver/cast.framework.CastReceiverOptions#uiConfig
|
||||
// This doesn't work either.
|
||||
// @ts-ignore
|
||||
options.uiConfig = new cast.framework.ui.UiConfig();
|
||||
// @ts-ignore
|
||||
options.uiConfig.touchScreenOptimizedApp = true;
|
||||
|
||||
castContext.addCustomMessageListener(
|
||||
CAST_NS,
|
||||
// @ts-ignore
|
||||
(ev: ReceivedMessage<HassMessage>) => {
|
||||
const msg = ev.data;
|
||||
msg.senderId = ev.senderId;
|
||||
controller.processIncomingMessage(msg);
|
||||
}
|
||||
);
|
||||
|
||||
castContext.start(options);
|
||||
56
cast/src/receiver/layout/hc-demo.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { HassElement } from "../../../../src/state/hass-element";
|
||||
import "./hc-lovelace";
|
||||
import { customElement, TemplateResult, html, property } from "lit-element";
|
||||
import {
|
||||
MockHomeAssistant,
|
||||
provideHass,
|
||||
} from "../../../../src/fake_data/provide_hass";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { LovelaceConfig } from "../../../../src/data/lovelace";
|
||||
import { castDemoEntities } from "../demo/cast-demo-entities";
|
||||
import { castDemoLovelace } from "../demo/cast-demo-lovelace";
|
||||
import { mockHistory } from "../../../../demo/src/stubs/history";
|
||||
|
||||
@customElement("hc-demo")
|
||||
class HcDemo extends HassElement {
|
||||
@property() public lovelacePath!: string;
|
||||
@property() private _lovelaceConfig?: LovelaceConfig;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._lovelaceConfig) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<hc-lovelace
|
||||
.hass=${this.hass}
|
||||
.lovelaceConfig=${this._lovelaceConfig}
|
||||
.viewPath=${this.lovelacePath}
|
||||
></hc-lovelace>
|
||||
`;
|
||||
}
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private async _initialize() {
|
||||
const initial: Partial<MockHomeAssistant> = {
|
||||
// Override updateHass so that the correct hass lifecycle methods are called
|
||||
updateHass: (hassUpdate: Partial<HomeAssistant>) =>
|
||||
this._updateHass(hassUpdate),
|
||||
};
|
||||
|
||||
const hass = (this.hass = provideHass(this, initial));
|
||||
|
||||
mockHistory(hass);
|
||||
|
||||
hass.addEntities(castDemoEntities());
|
||||
this._lovelaceConfig = castDemoLovelace();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hc-demo": HcDemo;
|
||||
}
|
||||
}
|
||||
66
cast/src/receiver/layout/hc-launch-screen.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
customElement,
|
||||
CSSResult,
|
||||
css,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
|
||||
@customElement("hc-launch-screen")
|
||||
class HcLaunchScreen extends LitElement {
|
||||
@property() public hass?: HomeAssistant;
|
||||
@property() public error?: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<div class="container">
|
||||
<img
|
||||
src="https://www.home-assistant.io/images/blog/2018-09-thinking-big/social.png"
|
||||
/>
|
||||
<div class="status">
|
||||
${this.hass ? "Connected" : "Not Connected"}
|
||||
${this.error
|
||||
? html`
|
||||
<p>Error: ${this.error}</p>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100vh;
|
||||
padding-top: 64px;
|
||||
background-color: white;
|
||||
font-size: 24px;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
width: 717px;
|
||||
height: 376px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.status {
|
||||
padding-right: 54px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hc-launch-screen": HcLaunchScreen;
|
||||
}
|
||||
}
|
||||
108
cast/src/receiver/layout/hc-lovelace.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
customElement,
|
||||
CSSResult,
|
||||
css,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { LovelaceConfig } from "../../../../src/data/lovelace";
|
||||
import "../../../../src/panels/lovelace/hui-view";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { Lovelace } from "../../../../src/panels/lovelace/types";
|
||||
import "./hc-launch-screen";
|
||||
|
||||
@customElement("hc-lovelace")
|
||||
class HcLovelace extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public lovelaceConfig!: LovelaceConfig;
|
||||
|
||||
@property() public viewPath?: string | number;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const index = this._viewIndex;
|
||||
if (index === undefined) {
|
||||
return html`
|
||||
<hc-launch-screen
|
||||
.hass=${this.hass}
|
||||
.error=${`Unable to find a view with path ${this.viewPath}`}
|
||||
></hc-launch-screen>
|
||||
`;
|
||||
}
|
||||
const lovelace: Lovelace = {
|
||||
config: this.lovelaceConfig,
|
||||
editMode: false,
|
||||
enableFullEditMode: () => undefined,
|
||||
mode: "storage",
|
||||
language: "en",
|
||||
saveConfig: async () => undefined,
|
||||
setEditMode: () => undefined,
|
||||
};
|
||||
return html`
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.lovelace=${lovelace}
|
||||
.index=${index}
|
||||
columns="2"
|
||||
></hui-view>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("viewPath") || changedProps.has("lovelaceConfig")) {
|
||||
const index = this._viewIndex;
|
||||
|
||||
if (index !== undefined) {
|
||||
const configBackground =
|
||||
this.lovelaceConfig.views[index].background ||
|
||||
this.lovelaceConfig.background;
|
||||
|
||||
if (configBackground) {
|
||||
this.shadowRoot!.querySelector("hui-view")!.style.setProperty(
|
||||
"--lovelace-background",
|
||||
configBackground
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private get _viewIndex() {
|
||||
const selectedView = this.viewPath;
|
||||
const selectedViewInt = parseInt(selectedView as string, 10);
|
||||
for (let i = 0; i < this.lovelaceConfig.views.length; i++) {
|
||||
if (
|
||||
this.lovelaceConfig.views[i].path === selectedView ||
|
||||
i === selectedViewInt
|
||||
) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
background: var(--primary-background-color);
|
||||
}
|
||||
hui-view {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hc-lovelace": HcLovelace;
|
||||
}
|
||||
}
|
||||
233
cast/src/receiver/layout/hc-main.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import {
|
||||
getAuth,
|
||||
createConnection,
|
||||
UnsubscribeFunc,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { customElement, TemplateResult, html, property } from "lit-element";
|
||||
import { HassElement } from "../../../../src/state/hass-element";
|
||||
import {
|
||||
HassMessage,
|
||||
ConnectMessage,
|
||||
ShowLovelaceViewMessage,
|
||||
GetStatusMessage,
|
||||
ShowDemoMessage,
|
||||
} from "../../../../src/cast/receiver_messages";
|
||||
import {
|
||||
LovelaceConfig,
|
||||
getLovelaceCollection,
|
||||
} from "../../../../src/data/lovelace";
|
||||
import "./hc-launch-screen";
|
||||
import { castContext } from "../cast_context";
|
||||
import { CAST_NS } from "../../../../src/cast/const";
|
||||
import { ReceiverStatusMessage } from "../../../../src/cast/sender_messages";
|
||||
import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/load-resources";
|
||||
import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click";
|
||||
|
||||
@customElement("hc-main")
|
||||
export class HcMain extends HassElement {
|
||||
@property() private _showDemo = false;
|
||||
|
||||
@property() private _lovelaceConfig?: LovelaceConfig;
|
||||
|
||||
@property() private _lovelacePath: string | number | null = null;
|
||||
|
||||
@property() private _error?: string;
|
||||
|
||||
private _unsubLovelace?: UnsubscribeFunc;
|
||||
|
||||
public processIncomingMessage(msg: HassMessage) {
|
||||
if (msg.type === "connect") {
|
||||
this._handleConnectMessage(msg);
|
||||
} else if (msg.type === "show_lovelace_view") {
|
||||
this._handleShowLovelaceMessage(msg);
|
||||
} else if (msg.type === "get_status") {
|
||||
this._handleGetStatusMessage(msg);
|
||||
} else if (msg.type === "show_demo") {
|
||||
this._handleShowDemo(msg);
|
||||
} else {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.warn("unknown msg type", msg);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (this._showDemo) {
|
||||
return html`
|
||||
<hc-demo .lovelacePath=${this._lovelacePath}></hc-demo>
|
||||
`;
|
||||
}
|
||||
|
||||
if (
|
||||
!this._lovelaceConfig ||
|
||||
this._lovelacePath === null ||
|
||||
// Guard against part of HA not being loaded yet.
|
||||
(this.hass &&
|
||||
(!this.hass.states || !this.hass.config || !this.hass.services))
|
||||
) {
|
||||
return html`
|
||||
<hc-launch-screen
|
||||
.hass=${this.hass}
|
||||
.error=${this._error}
|
||||
></hc-launch-screen>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<hc-lovelace
|
||||
.hass=${this.hass}
|
||||
.lovelaceConfig=${this._lovelaceConfig}
|
||||
.viewPath=${this._lovelacePath}
|
||||
></hc-lovelace>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
import("../second-load");
|
||||
window.addEventListener("location-changed", () => {
|
||||
if (location.pathname.startsWith("/lovelace/")) {
|
||||
this._lovelacePath = location.pathname.substr(10);
|
||||
this._sendStatus();
|
||||
}
|
||||
});
|
||||
document.body.addEventListener("click", (ev) => {
|
||||
const href = isNavigationClick(ev);
|
||||
if (href && href.startsWith("/lovelace/")) {
|
||||
this._lovelacePath = href.substr(10);
|
||||
this._sendStatus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _sendStatus(senderId?: string) {
|
||||
const status: ReceiverStatusMessage = {
|
||||
type: "receiver_status",
|
||||
connected: !!this.hass,
|
||||
showDemo: this._showDemo,
|
||||
};
|
||||
|
||||
if (this.hass) {
|
||||
status.hassUrl = this.hass.auth.data.hassUrl;
|
||||
status.lovelacePath = this._lovelacePath!;
|
||||
}
|
||||
|
||||
if (senderId) {
|
||||
this.sendMessage(senderId, status);
|
||||
} else {
|
||||
for (const sender of castContext.getSenders()) {
|
||||
this.sendMessage(sender.id, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleGetStatusMessage(msg: GetStatusMessage) {
|
||||
this._sendStatus(msg.senderId!);
|
||||
}
|
||||
|
||||
private async _handleConnectMessage(msg: ConnectMessage) {
|
||||
let auth;
|
||||
try {
|
||||
auth = await getAuth({
|
||||
loadTokens: async () => ({
|
||||
hassUrl: msg.hassUrl,
|
||||
clientId: msg.clientId,
|
||||
refresh_token: msg.refreshToken,
|
||||
access_token: "",
|
||||
expires: 0,
|
||||
expires_in: 0,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
this._error = err;
|
||||
return;
|
||||
}
|
||||
let connection;
|
||||
try {
|
||||
connection = await createConnection({ auth });
|
||||
} catch (err) {
|
||||
this._error = err;
|
||||
return;
|
||||
}
|
||||
if (this.hass) {
|
||||
this.hass.connection.close();
|
||||
}
|
||||
this.initializeHass(auth, connection);
|
||||
this._error = undefined;
|
||||
this._sendStatus();
|
||||
}
|
||||
|
||||
private async _handleShowLovelaceMessage(msg: ShowLovelaceViewMessage) {
|
||||
// We should not get this command before we are connected.
|
||||
// Means a client got out of sync. Let's send status to them.
|
||||
if (!this.hass) {
|
||||
this._sendStatus(msg.senderId!);
|
||||
this._error = "Cannot show Lovelace because we're not connected.";
|
||||
return;
|
||||
}
|
||||
if (!this._unsubLovelace) {
|
||||
const llColl = getLovelaceCollection(this.hass!.connection);
|
||||
// We first do a single refresh because we need to check if there is LL
|
||||
// configuration.
|
||||
try {
|
||||
await llColl.refresh();
|
||||
this._unsubLovelace = llColl.subscribe((lovelaceConfig) =>
|
||||
this._handleNewLovelaceConfig(lovelaceConfig)
|
||||
);
|
||||
} catch (err) {
|
||||
// Generate a Lovelace config.
|
||||
this._unsubLovelace = () => undefined;
|
||||
const {
|
||||
generateLovelaceConfigFromHass,
|
||||
} = await import("../../../../src/panels/lovelace/common/generate-lovelace-config");
|
||||
this._handleNewLovelaceConfig(
|
||||
await generateLovelaceConfigFromHass(this.hass!)
|
||||
);
|
||||
}
|
||||
}
|
||||
this._showDemo = false;
|
||||
this._lovelacePath = msg.viewPath;
|
||||
if (castContext.getDeviceCapabilities().touch_input_supported) {
|
||||
this._breakFree();
|
||||
}
|
||||
this._sendStatus();
|
||||
}
|
||||
|
||||
private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) {
|
||||
castContext.setApplicationState(lovelaceConfig.title!);
|
||||
this._lovelaceConfig = lovelaceConfig;
|
||||
if (lovelaceConfig.resources) {
|
||||
loadLovelaceResources(
|
||||
lovelaceConfig.resources,
|
||||
this.hass!.auth.data.hassUrl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleShowDemo(_msg: ShowDemoMessage) {
|
||||
import("./hc-demo").then(() => {
|
||||
this._showDemo = true;
|
||||
this._lovelacePath = "overview";
|
||||
this._sendStatus();
|
||||
if (castContext.getDeviceCapabilities().touch_input_supported) {
|
||||
this._breakFree();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _breakFree() {
|
||||
const controls = document.body.querySelector("touch-controls");
|
||||
if (controls) {
|
||||
controls.remove();
|
||||
}
|
||||
document.body.setAttribute("style", "overflow-y: auto !important");
|
||||
}
|
||||
|
||||
private sendMessage(senderId: string, response: any) {
|
||||
castContext.sendCustomMessage(CAST_NS, senderId, response);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hc-main": HcMain;
|
||||
}
|
||||
}
|
||||
5
cast/src/receiver/second-load.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import "web-animations-js/web-animations-next-lite.min";
|
||||
import "../../../src/resources/hass-icons";
|
||||
import "../../../src/resources/roboto";
|
||||
import "../../../src/components/ha-iconset-svg";
|
||||
import "./layout/hc-lovelace";
|
||||
6
cast/src/receiver/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface ReceivedMessage<T> {
|
||||
gj: boolean;
|
||||
data: T;
|
||||
senderId: string;
|
||||
type: "message";
|
||||
}
|
||||
BIN
demo/public/api/camera_proxy_stream/camera.backyard
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
demo/public/api/camera_proxy_stream/camera.driveway
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
demo/public/api/camera_proxy_stream/camera.patio
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
demo/public/api/camera_proxy_stream/camera.porch
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
demo/public/assets/arsaboo/floorplans/ecobee_blank.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
demo/public/assets/arsaboo/floorplans/main.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
demo/public/assets/arsaboo/floorplans/second.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
demo/public/assets/arsaboo/icons/Harmony.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
demo/public/assets/arsaboo/icons/abode_disabled.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
demo/public/assets/arsaboo/icons/abode_enabled.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
demo/public/assets/arsaboo/icons/automation_disabled.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
demo/public/assets/arsaboo/icons/automation_enabled.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
demo/public/assets/arsaboo/icons/camera_backyard_recording.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
demo/public/assets/arsaboo/icons/camera_backyard_streaming.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
demo/public/assets/arsaboo/icons/camera_driveway_recording.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
demo/public/assets/arsaboo/icons/camera_driveway_streaming.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
demo/public/assets/arsaboo/icons/camera_patio_recording.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
demo/public/assets/arsaboo/icons/camera_patio_streaming.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
demo/public/assets/arsaboo/icons/camera_porch_recording.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
demo/public/assets/arsaboo/icons/camera_porch_streaming.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
demo/public/assets/arsaboo/icons/ecobee_blank.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
demo/public/assets/arsaboo/icons/garage_door_closed.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
demo/public/assets/arsaboo/icons/garage_door_open.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
demo/public/assets/arsaboo/icons/light_bulb_off.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
demo/public/assets/arsaboo/icons/light_bulb_on.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
demo/public/assets/arsaboo/icons/light_off.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
demo/public/assets/arsaboo/icons/light_on.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
demo/public/assets/arsaboo/icons/security_armed_red.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
demo/public/assets/arsaboo/icons/security_disarmed.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
demo/public/assets/arsaboo/icons/tv_disabled.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
demo/public/assets/arsaboo/icons/tv_enabled.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
demo/public/assets/arsaboo/icons/tv_off2.png
Normal file
|
After Width: | Height: | Size: 767 B |
BIN
demo/public/assets/arsaboo/icons/tv_on2.png
Normal file
|
After Width: | Height: | Size: 805 B |
BIN
demo/public/assets/arsaboo/images/arsaboo.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
demo/public/assets/arsaboo/images/melody.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
demo/public/assets/jimpower/background-15.jpg
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
demo/public/assets/jimpower/cardbackK.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
demo/public/assets/jimpower/home/bus_10.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
demo/public/assets/jimpower/home/git.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
demo/public/assets/jimpower/home/house_4.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
demo/public/assets/jimpower/home/james_10.jpg
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
demo/public/assets/jimpower/home/tina_4.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
demo/public/assets/jimpower/security/air_8.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
demo/public/assets/jimpower/security/alarm_3.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
demo/public/assets/jimpower/security/door_3.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
demo/public/assets/jimpower/security/leak_2.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
demo/public/assets/jimpower/security/motion_3.jpg
Normal file
|
After Width: | Height: | Size: 87 KiB |