forked from home-assistant/core
Compare commits
756 Commits
2021.2.2
...
2021.3.0b3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdf7372fd8 | ||
|
|
0969cc985b | ||
|
|
5e2bafca56 | ||
|
|
1d1be8ad1a | ||
|
|
d55f0df09a | ||
|
|
96e118ccfe | ||
|
|
255b6faa7f | ||
|
|
4cd40d0f9f | ||
|
|
c12769213d | ||
|
|
6a850a1481 | ||
|
|
101897c260 | ||
|
|
6cdd6c3f44 | ||
|
|
2c30579a11 | ||
|
|
ae0d301fd9 | ||
|
|
a7a66e8ddb | ||
|
|
5228bbd43c | ||
|
|
35bce434cc | ||
|
|
33a6fb1baf | ||
|
|
399c299cf2 | ||
|
|
c797e0c8de | ||
|
|
cb2dd6d908 | ||
|
|
a3cde9b19e | ||
|
|
a58931280a | ||
|
|
2fef4c4eef | ||
|
|
567ec26c48 | ||
|
|
23cbd2dda3 | ||
|
|
afae253432 | ||
|
|
4c294adfe8 | ||
|
|
8d2606134d | ||
|
|
ba51ada494 | ||
|
|
2f1dba74d1 | ||
|
|
39baeb62f2 | ||
|
|
daf7595ca6 | ||
|
|
783e0f9a14 | ||
|
|
868a536d81 | ||
|
|
0eb8951aed | ||
|
|
722b1e8746 | ||
|
|
db0d815f9d | ||
|
|
106ae18432 | ||
|
|
11a89bc3ac | ||
|
|
0ef16dd563 | ||
|
|
db8f597f10 | ||
|
|
424526db7e | ||
|
|
44293a3738 | ||
|
|
470121e5b0 | ||
|
|
b645b151f9 | ||
|
|
8f19d041e4 | ||
|
|
45b50c53ad | ||
|
|
b0d56970a5 | ||
|
|
17f4c9dd06 | ||
|
|
1a73cb4791 | ||
|
|
1c7c6163dd | ||
|
|
eccdae60bf | ||
|
|
a632215541 | ||
|
|
42fd3be0e8 | ||
|
|
3e26e2adad | ||
|
|
19f5b467b7 | ||
|
|
b8f7bc12ee | ||
|
|
d4d68ebc64 | ||
|
|
d02b27a5d0 | ||
|
|
87cbbcb014 | ||
|
|
9159f54900 | ||
|
|
b657fd02cd | ||
|
|
1a99562e91 | ||
|
|
228096847b | ||
|
|
425d56d024 | ||
|
|
c9847920af | ||
|
|
23b2953773 | ||
|
|
d96249e39c | ||
|
|
b583ded8b5 | ||
|
|
d68a51ddce | ||
|
|
00dd557cce | ||
|
|
089effbe3f | ||
|
|
272e975a52 | ||
|
|
eb8d723689 | ||
|
|
6a8b5ee51b | ||
|
|
ffe42e150a | ||
|
|
5a3bd30e01 | ||
|
|
c94968d811 | ||
|
|
7a7147edcf | ||
|
|
3c35b6558b | ||
|
|
f0f752936b | ||
|
|
afa91e886b | ||
|
|
ea4bbd771f | ||
|
|
593e7aea5a | ||
|
|
6fe72b04eb | ||
|
|
4fdb617e22 | ||
|
|
08889a9819 | ||
|
|
f33618d33d | ||
|
|
bd87047ff2 | ||
|
|
fb2a100f5e | ||
|
|
f005c68630 | ||
|
|
1cecf229b9 | ||
|
|
580d25c622 | ||
|
|
20ccec9aab | ||
|
|
9d7c64ec1a | ||
|
|
04e07d8b2c | ||
|
|
be33336d96 | ||
|
|
8ac9faef3b | ||
|
|
f0c7aff248 | ||
|
|
fb32c2e3a8 | ||
|
|
e70d896e1b | ||
|
|
668574c48f | ||
|
|
5907129b25 | ||
|
|
6e10b39d67 | ||
|
|
692942b399 | ||
|
|
c8ffac20b9 | ||
|
|
75e04f3a71 | ||
|
|
81d011efc5 | ||
|
|
603191702f | ||
|
|
82a9dc620c | ||
|
|
36b56586de | ||
|
|
338c07a56b | ||
|
|
23c2bd4e69 | ||
|
|
e5aef45bd7 | ||
|
|
d61d39de08 | ||
|
|
75b37b4c2a | ||
|
|
b2b476596b | ||
|
|
5c29adea3d | ||
|
|
5cd022a683 | ||
|
|
1a27af43cc | ||
|
|
5d8390fd9b | ||
|
|
b1a24c8bbb | ||
|
|
b6b1e725c7 | ||
|
|
a8be5be376 | ||
|
|
d32dbc4cdd | ||
|
|
0e44d61225 | ||
|
|
12c4db076c | ||
|
|
8330940996 | ||
|
|
29c0696537 | ||
|
|
871427f5f1 | ||
|
|
50a07f6d25 | ||
|
|
c1ee9f7e4a | ||
|
|
d33a1a5ff8 | ||
|
|
e2fd255a96 | ||
|
|
d9ab1482bc | ||
|
|
5e26bda52d | ||
|
|
2d70806035 | ||
|
|
3ad207a499 | ||
|
|
efa339ca54 | ||
|
|
0cb1f61deb | ||
|
|
f045c0512b | ||
|
|
115fe26642 | ||
|
|
2b6619f815 | ||
|
|
4af619d383 | ||
|
|
6e52b26c06 | ||
|
|
43a5852561 | ||
|
|
fe4cf611f7 | ||
|
|
806369ddc1 | ||
|
|
6bb848455f | ||
|
|
194c0d2b08 | ||
|
|
2cf46330b1 | ||
|
|
c6c0e2416c | ||
|
|
9f4874bb81 | ||
|
|
cf69415272 | ||
|
|
2ae790283c | ||
|
|
2940df09e9 | ||
|
|
da1808226e | ||
|
|
39da0dc409 | ||
|
|
adf480025d | ||
|
|
65e8835f28 | ||
|
|
b5c7493b60 | ||
|
|
4aa4f7e285 | ||
|
|
788134cbc4 | ||
|
|
26ce316c18 | ||
|
|
b775a0d796 | ||
|
|
41332493b5 | ||
|
|
9b69549f73 | ||
|
|
5b0b01d727 | ||
|
|
6707496c5d | ||
|
|
5b95f61fd3 | ||
|
|
773a202777 | ||
|
|
749883dc62 | ||
|
|
500cb17298 | ||
|
|
22dbac259b | ||
|
|
4078a8782e | ||
|
|
11277faa93 | ||
|
|
2807cb1de7 | ||
|
|
3e334a4950 | ||
|
|
0e9148e239 | ||
|
|
54cf954353 | ||
|
|
2f3c2f5f4d | ||
|
|
71586b7661 | ||
|
|
bb7e4d7daa | ||
|
|
e6125a1e4e | ||
|
|
fd60d4273b | ||
|
|
47dcd2bf32 | ||
|
|
4d23ffacd1 | ||
|
|
32bec5ea63 | ||
|
|
250327fac4 | ||
|
|
3e82509263 | ||
|
|
6ad7020f99 | ||
|
|
d2a187a57b | ||
|
|
bfea7d0baa | ||
|
|
e7e3e09063 | ||
|
|
40068c2f1b | ||
|
|
0ed0c7c026 | ||
|
|
f2b303d509 | ||
|
|
5df46b60e8 | ||
|
|
da51e23514 | ||
|
|
d9ce7db554 | ||
|
|
52a9a66d0f | ||
|
|
76e5f86b76 | ||
|
|
8b97f62a8e | ||
|
|
3353c63f8f | ||
|
|
01ebb156a0 | ||
|
|
782863ca87 | ||
|
|
b6f374b507 | ||
|
|
113059c1c7 | ||
|
|
e000b9c813 | ||
|
|
a80921ab65 | ||
|
|
b9d5b3c34e | ||
|
|
8b69608242 | ||
|
|
208af0367a | ||
|
|
81c7b3b9c9 | ||
|
|
a164a6cf80 | ||
|
|
cdc4f634d1 | ||
|
|
3c7db7bd5b | ||
|
|
a5ac338c74 | ||
|
|
4b6b03e33e | ||
|
|
e57bfe05d5 | ||
|
|
cd6a83f009 | ||
|
|
3f96ebeae5 | ||
|
|
62e0949ea9 | ||
|
|
0f4433ce14 | ||
|
|
b4136c3585 | ||
|
|
c0cdc0fe79 | ||
|
|
1d62bf8875 | ||
|
|
74720d4afd | ||
|
|
bfa171f802 | ||
|
|
12477c5e46 | ||
|
|
616e8f6a65 | ||
|
|
88d143a644 | ||
|
|
62cfe24ed4 | ||
|
|
82934b31f8 | ||
|
|
4083b90138 | ||
|
|
0181cbb312 | ||
|
|
399777cfa8 | ||
|
|
ae643bfaf3 | ||
|
|
bc1cb8f0a0 | ||
|
|
e45fc4562b | ||
|
|
2dfbd4fbcf | ||
|
|
4e93a0c774 | ||
|
|
c8c3ce4172 | ||
|
|
dec2eb36fd | ||
|
|
9f3fdb1b68 | ||
|
|
da662c4890 | ||
|
|
39785c5cef | ||
|
|
e9334347eb | ||
|
|
28ffa97635 | ||
|
|
2ac075bb37 | ||
|
|
8bee3cda37 | ||
|
|
b2df9aaaf1 | ||
|
|
4ab0151fb1 | ||
|
|
1e9483a0e9 | ||
|
|
eb3e5cb67f | ||
|
|
f7c0fc5553 | ||
|
|
facbd73130 | ||
|
|
fb73768164 | ||
|
|
efb172cedd | ||
|
|
971e27dd80 | ||
|
|
ddf1f88b65 | ||
|
|
94131df5e0 | ||
|
|
8c72cb6163 | ||
|
|
56f32196bd | ||
|
|
b956a571f4 | ||
|
|
58f6db0127 | ||
|
|
58499946ed | ||
|
|
4236f6e5d4 | ||
|
|
318cbf2913 | ||
|
|
b38af0821b | ||
|
|
e4496ed1e3 | ||
|
|
aaecd91407 | ||
|
|
c45ce86e53 | ||
|
|
960b5b7d86 | ||
|
|
08201d146b | ||
|
|
68e78a2ddc | ||
|
|
f0e9ef421c | ||
|
|
9d8ba6af96 | ||
|
|
add0d9d3eb | ||
|
|
713544e5eb | ||
|
|
a6912277eb | ||
|
|
0bfcd5e1ee | ||
|
|
6986fa4eb6 | ||
|
|
20d93b4b29 | ||
|
|
1e172dedf6 | ||
|
|
3c26235e78 | ||
|
|
1bb535aa67 | ||
|
|
2a7d2868be | ||
|
|
ea47e5d8af | ||
|
|
6f4df7e52e | ||
|
|
e3ae3cfb83 | ||
|
|
2f9fda73f4 | ||
|
|
68809e9f43 | ||
|
|
89aaeb3c35 | ||
|
|
f2ca4acff0 | ||
|
|
886067a327 | ||
|
|
a5d943b5f1 | ||
|
|
9917bb76fb | ||
|
|
c5b9ad83c2 | ||
|
|
bed29fd4b1 | ||
|
|
c8e04ee960 | ||
|
|
5a907ebafc | ||
|
|
3f4828f5e1 | ||
|
|
cfdaadf5d9 | ||
|
|
06c8fc6ef1 | ||
|
|
a3b733f1ec | ||
|
|
0af634a9f8 | ||
|
|
12abe5707d | ||
|
|
7e88487800 | ||
|
|
a5f372018c | ||
|
|
f1c792b4c8 | ||
|
|
aa061e5818 | ||
|
|
1444afbe5a | ||
|
|
c9df42b69a | ||
|
|
f8f86fbe48 | ||
|
|
855bd653b4 | ||
|
|
9777608861 | ||
|
|
27d16af36b | ||
|
|
a5a45f29e2 | ||
|
|
10e88cd23d | ||
|
|
dfa973f9ef | ||
|
|
accba85e35 | ||
|
|
2c3a2bd35e | ||
|
|
811e1cc3e6 | ||
|
|
3ea02e646d | ||
|
|
294d3c6529 | ||
|
|
854504cccc | ||
|
|
c76758f775 | ||
|
|
7a401d3d5d | ||
|
|
1845f69729 | ||
|
|
17a4678906 | ||
|
|
dfe173d619 | ||
|
|
5db4d78dc7 | ||
|
|
84488b9c28 | ||
|
|
7148071be8 | ||
|
|
eecf07d7df | ||
|
|
2f40f44670 | ||
|
|
f38b06ed6d | ||
|
|
6f261a09b0 | ||
|
|
52c5bc0a99 | ||
|
|
bc1daf1802 | ||
|
|
1244fb4152 | ||
|
|
13b881acfc | ||
|
|
621c8e700b | ||
|
|
b8584cab5d | ||
|
|
820a260252 | ||
|
|
2ecac6550f | ||
|
|
1a8cdba9af | ||
|
|
8bacfcec50 | ||
|
|
ae45d7dade | ||
|
|
da4cb6d294 | ||
|
|
bc8a52038b | ||
|
|
dd8d4471ec | ||
|
|
061d9c5293 | ||
|
|
362a1cd9bd | ||
|
|
f1714dd541 | ||
|
|
8418489345 | ||
|
|
f929aa222f | ||
|
|
c3b460920e | ||
|
|
a8beae3c51 | ||
|
|
479ff92acb | ||
|
|
b7dd9bf58f | ||
|
|
74f5f8976f | ||
|
|
190a9f66cb | ||
|
|
0d2f5cf7ed | ||
|
|
9b7c39d20b | ||
|
|
910c034613 | ||
|
|
a67b598971 | ||
|
|
ee04473e85 | ||
|
|
34a491f826 | ||
|
|
8dc06e612f | ||
|
|
14a64ea970 | ||
|
|
26e7916367 | ||
|
|
70e23402a9 | ||
|
|
eb0d1bb673 | ||
|
|
fd177441b3 | ||
|
|
c95f401e2e | ||
|
|
ed31cc363b | ||
|
|
888c9e120d | ||
|
|
5ce49c62b1 | ||
|
|
b1a7bfee14 | ||
|
|
c75e63dc95 | ||
|
|
b85ecc0bd2 | ||
|
|
c10bf079f7 | ||
|
|
1b61b5c10b | ||
|
|
4034a274f7 | ||
|
|
6015161dab | ||
|
|
953491d509 | ||
|
|
b5061d0232 | ||
|
|
291746334a | ||
|
|
9f839b8c61 | ||
|
|
70af3e4776 | ||
|
|
1c2f72a453 | ||
|
|
bf2a34600a | ||
|
|
b5bdd7f2cf | ||
|
|
30ddfd837a | ||
|
|
d2d2bed16b | ||
|
|
db557a094c | ||
|
|
1f5fb8f28a | ||
|
|
e013ad2413 | ||
|
|
f9f4c0aeed | ||
|
|
29d8b8a22f | ||
|
|
379f5455e5 | ||
|
|
f549ec5ec9 | ||
|
|
3ffa42e56a | ||
|
|
56adc9dadb | ||
|
|
af2fa17e8e | ||
|
|
7f8fa7feaf | ||
|
|
8007391244 | ||
|
|
6182fedf3f | ||
|
|
281fbe1dfa | ||
|
|
d8a2e0e051 | ||
|
|
fdd8159955 | ||
|
|
acde33dbbc | ||
|
|
9bc3c6c130 | ||
|
|
c59b1c72c5 | ||
|
|
67ab86443e | ||
|
|
884df40951 | ||
|
|
2e2eab662b | ||
|
|
fc4fc48763 | ||
|
|
2db102e023 | ||
|
|
cdd78316c4 | ||
|
|
538df17a28 | ||
|
|
74647e1fa8 | ||
|
|
917a616ce1 | ||
|
|
dbb98e6cac | ||
|
|
ea4ad85488 | ||
|
|
7928cda080 | ||
|
|
ad72715212 | ||
|
|
b7e11347d5 | ||
|
|
c66d9ea25c | ||
|
|
ad400d91bc | ||
|
|
6e1f3b7861 | ||
|
|
22389043eb | ||
|
|
a6358430b4 | ||
|
|
4b493c5ab9 | ||
|
|
7d2d98fc3c | ||
|
|
bfd5a62bad | ||
|
|
1fea24502c | ||
|
|
78b7fbf7b1 | ||
|
|
b0b81246f0 | ||
|
|
175f2f0275 | ||
|
|
26f455223b | ||
|
|
00aebec90d | ||
|
|
5fcb948e28 | ||
|
|
3381e2f65a | ||
|
|
6f4cb18fa8 | ||
|
|
a26cf7aeec | ||
|
|
1c1b2f497a | ||
|
|
57ce182959 | ||
|
|
f46dc3c48e | ||
|
|
c69c493cf9 | ||
|
|
da67cde369 | ||
|
|
f27066e773 | ||
|
|
6a62ebb6a4 | ||
|
|
2fc1c19a45 | ||
|
|
20f45f8ab9 | ||
|
|
b33753f334 | ||
|
|
2bcf87b980 | ||
|
|
889baef456 | ||
|
|
936ee7d733 | ||
|
|
6563c37ab1 | ||
|
|
93fafedf72 | ||
|
|
58b4a91a5b | ||
|
|
c602c619a2 | ||
|
|
6467eff09c | ||
|
|
dc26fd5149 | ||
|
|
6b340415b2 | ||
|
|
c2302784c2 | ||
|
|
00bbf8c3a2 | ||
|
|
c0a1fc2916 | ||
|
|
fcae840641 | ||
|
|
71d7ae5992 | ||
|
|
829131fe51 | ||
|
|
be779d8712 | ||
|
|
e27619fe50 | ||
|
|
1b194e3b2f | ||
|
|
86fe5d0561 | ||
|
|
dca6a93898 | ||
|
|
e20a814926 | ||
|
|
81c88cd639 | ||
|
|
8f4ea3818d | ||
|
|
b1ffe429cd | ||
|
|
2811e39c5c | ||
|
|
48808978c4 | ||
|
|
568180632e | ||
|
|
6f446cf627 | ||
|
|
48002f47f4 | ||
|
|
0780e52ca4 | ||
|
|
eaa2d371a7 | ||
|
|
2744d64a3e | ||
|
|
9d9c4b47ee | ||
|
|
92e5bf9786 | ||
|
|
82607977ef | ||
|
|
54dce1c505 | ||
|
|
5a4e1eeb0e | ||
|
|
a23e05d1f6 | ||
|
|
87c36d6b6b | ||
|
|
5faf463205 | ||
|
|
9b0955b67e | ||
|
|
e7ca0ff71a | ||
|
|
f99c27c6d4 | ||
|
|
8efb5eea4d | ||
|
|
047f16772f | ||
|
|
b9b1caf4d7 | ||
|
|
9e07910ab0 | ||
|
|
aa005af266 | ||
|
|
c7a9571920 | ||
|
|
75519d2d6c | ||
|
|
352bba1f15 | ||
|
|
840891e4f4 | ||
|
|
28ef3f68f3 | ||
|
|
ca87bf49b6 | ||
|
|
04c1578f15 | ||
|
|
5755598c95 | ||
|
|
66ecd2e0f2 | ||
|
|
d02b78c634 | ||
|
|
aa00c62302 | ||
|
|
8e06fa017d | ||
|
|
19e9515bec | ||
|
|
74053b5f2d | ||
|
|
94eb31025c | ||
|
|
818501216e | ||
|
|
618fcda821 | ||
|
|
cefde8721d | ||
|
|
60e3fce7dc | ||
|
|
08163a848c | ||
|
|
242ff045b9 | ||
|
|
fb68bf85ae | ||
|
|
94ecb792ec | ||
|
|
a74ae3585a | ||
|
|
369616a6c3 | ||
|
|
af4e6f856f | ||
|
|
ee98ea89dd | ||
|
|
8a7e0241ab | ||
|
|
f2d9e6f70c | ||
|
|
ce159d7db3 | ||
|
|
33169cf8cd | ||
|
|
67392338da | ||
|
|
434b4dfa58 | ||
|
|
2c74befd4f | ||
|
|
c01e01f797 | ||
|
|
0d620eb7c3 | ||
|
|
55f9d98523 | ||
|
|
55f81a8a04 | ||
|
|
ae2c7e4c74 | ||
|
|
7144c5f316 | ||
|
|
6fdb12c09d | ||
|
|
9a570d7c32 | ||
|
|
e01ca40d56 | ||
|
|
2a0c36589f | ||
|
|
725dcb5cac | ||
|
|
6404f91d00 | ||
|
|
92886cafe9 | ||
|
|
2b17ba1dc4 | ||
|
|
6e9aa254d5 | ||
|
|
bcefbe2dca | ||
|
|
374aa3aee1 | ||
|
|
d1b7d25a5d | ||
|
|
c6bd5b1b71 | ||
|
|
01e73911d6 | ||
|
|
097a4e6b59 | ||
|
|
62de921422 | ||
|
|
b9f9de0c1d | ||
|
|
60268e63d9 | ||
|
|
5d3dcff7c9 | ||
|
|
c7febacd9f | ||
|
|
912b816117 | ||
|
|
7b280bdbe7 | ||
|
|
56b8e82a69 | ||
|
|
b80c1688ad | ||
|
|
61a987061e | ||
|
|
134b1d3f63 | ||
|
|
1a74709757 | ||
|
|
9c6c2a77ab | ||
|
|
d44c941efe | ||
|
|
8625b772e3 | ||
|
|
afa7fd923a | ||
|
|
dd150bb797 | ||
|
|
fefe4a2021 | ||
|
|
7e9500e465 | ||
|
|
06e6005fbb | ||
|
|
8256acb8ef | ||
|
|
1b6ee8301a | ||
|
|
14d914e300 | ||
|
|
5a0715d388 | ||
|
|
44914c01ac | ||
|
|
04f39d7dd4 | ||
|
|
adf38f7074 | ||
|
|
51e695fd45 | ||
|
|
a775b79d4b | ||
|
|
4b208746e5 | ||
|
|
a584ad5ac3 | ||
|
|
6458ff774f | ||
|
|
0875f654c8 | ||
|
|
fcc14933d0 | ||
|
|
90973f471f | ||
|
|
5615ab4c25 | ||
|
|
9998fe3684 | ||
|
|
eaa9fff3ba | ||
|
|
40ba182144 | ||
|
|
a2ec1a47d5 | ||
|
|
959ed6d077 | ||
|
|
45ac6df76f | ||
|
|
048f36c77e | ||
|
|
d9dba1b7ab | ||
|
|
bf9b3bf9db | ||
|
|
524b9e7b1f | ||
|
|
2e98cfb9ab | ||
|
|
c93fec34b3 | ||
|
|
811bbb7acb | ||
|
|
63cc2517dd | ||
|
|
6e205965ee | ||
|
|
e9b2d33ad8 | ||
|
|
7ff4281b6d | ||
|
|
0382c93283 | ||
|
|
d417ee2732 | ||
|
|
a96a80e78d | ||
|
|
463a32819c | ||
|
|
a64ad50b27 | ||
|
|
0e3ba532c7 | ||
|
|
b3e2f8f904 | ||
|
|
0feda9ce63 | ||
|
|
8d9b66e23d | ||
|
|
3ef7bd6b73 | ||
|
|
411c0a9685 | ||
|
|
aea8636c7e | ||
|
|
60d4dadcb6 | ||
|
|
3a77ef02e4 | ||
|
|
253ae3f423 | ||
|
|
f2286d4811 | ||
|
|
d38d8a542d | ||
|
|
b4559a172c | ||
|
|
8222eb5e3e | ||
|
|
3bdf962838 | ||
|
|
197c857e1f | ||
|
|
776b1395de | ||
|
|
c90588d35d | ||
|
|
285bd3aa91 | ||
|
|
83a75b02ea | ||
|
|
2136b3013f | ||
|
|
6e67b943da | ||
|
|
374817fbaa | ||
|
|
a8cf377ed7 | ||
|
|
9f59515bb8 | ||
|
|
31a84555b9 | ||
|
|
e0bf18986b | ||
|
|
91a54eecb3 | ||
|
|
0b63510cab | ||
|
|
2ffdc4694a | ||
|
|
03928dbe55 | ||
|
|
1d94c10bb5 | ||
|
|
3e080f88c6 | ||
|
|
385b7e17ef | ||
|
|
73d7d80731 | ||
|
|
852af7e372 | ||
|
|
dac9626112 | ||
|
|
8be357ff4f | ||
|
|
868e530cbb | ||
|
|
e506d8616f | ||
|
|
c74ddf4720 | ||
|
|
f1d3af1a13 | ||
|
|
2d10c83150 | ||
|
|
ee55223065 | ||
|
|
ca43b3a8bb | ||
|
|
78934af6e6 | ||
|
|
f372bcf306 | ||
|
|
275946b96d | ||
|
|
ea7aa6af59 | ||
|
|
6b44636344 | ||
|
|
d13b58a4e6 | ||
|
|
e43cee163f | ||
|
|
da29855967 | ||
|
|
27407c1160 | ||
|
|
726bc6210b | ||
|
|
8a6469cfce | ||
|
|
63fb8307fb | ||
|
|
b00086ca1f | ||
|
|
88c4031e57 | ||
|
|
1fd3a86239 | ||
|
|
0964393002 | ||
|
|
b80571519b | ||
|
|
d81017f62e | ||
|
|
6bf59dbeab | ||
|
|
85e6bc581f | ||
|
|
07a4422a70 | ||
|
|
8a112721fa | ||
|
|
87d40ff815 | ||
|
|
41e2e5043b | ||
|
|
f07ffee535 | ||
|
|
adf8873e56 | ||
|
|
14c2053841 | ||
|
|
84f506efb7 | ||
|
|
ace5b58337 | ||
|
|
df00f32dfc | ||
|
|
48e899ca3a | ||
|
|
0fe3d6ea81 | ||
|
|
d4f186078c | ||
|
|
fbffea6b61 | ||
|
|
97fd05eb9f | ||
|
|
af68d5fb41 | ||
|
|
bcc9add0b4 | ||
|
|
b2789621bd | ||
|
|
5f9a1d105c | ||
|
|
bc3610c8e1 | ||
|
|
c1586f97db | ||
|
|
3f67f9e09c | ||
|
|
b8ff112920 | ||
|
|
da713e206d | ||
|
|
71c169c84f | ||
|
|
ba55f1ff4b | ||
|
|
b74cbb2a59 | ||
|
|
25c5c6aec9 | ||
|
|
f080af698d | ||
|
|
fb884e3afd | ||
|
|
c6105900f6 | ||
|
|
aacf6bd100 | ||
|
|
c66a892233 | ||
|
|
4f3b10d661 | ||
|
|
c7db2c35b7 | ||
|
|
8b3156ea82 | ||
|
|
eb370e9494 | ||
|
|
1edae8cd48 | ||
|
|
73cce8e8e2 | ||
|
|
7bc8060122 | ||
|
|
8bcb4092df | ||
|
|
e7ddaec468 | ||
|
|
d148f9aa85 | ||
|
|
d7e0391e03 | ||
|
|
8065ece0bd | ||
|
|
0da4034179 | ||
|
|
fdcf1fccf8 | ||
|
|
a4c8cb6f84 | ||
|
|
f0e4510213 | ||
|
|
92efe4f491 | ||
|
|
3ff75eee53 | ||
|
|
f1c24939f3 | ||
|
|
38d2cacf7a | ||
|
|
e4a7692610 | ||
|
|
ab1d42950a | ||
|
|
babfef829d | ||
|
|
22e44e4ba4 | ||
|
|
ee592350b3 | ||
|
|
85e463d507 | ||
|
|
0693d8a064 | ||
|
|
3896e81db7 | ||
|
|
0441960ffd | ||
|
|
0ec068667f | ||
|
|
e43d865112 | ||
|
|
92e084cee1 | ||
|
|
7673f57248 | ||
|
|
5711d61b38 | ||
|
|
068d1b5eb8 | ||
|
|
3f948e027a | ||
|
|
0338f5bccf |
71
.coveragerc
71
.coveragerc
@@ -23,6 +23,8 @@ omit =
|
||||
homeassistant/components/adguard/sensor.py
|
||||
homeassistant/components/adguard/switch.py
|
||||
homeassistant/components/ads/*
|
||||
homeassistant/components/aemet/abstract_aemet_sensor.py
|
||||
homeassistant/components/aemet/weather_update_coordinator.py
|
||||
homeassistant/components/aftership/sensor.py
|
||||
homeassistant/components/agent_dvr/__init__.py
|
||||
homeassistant/components/agent_dvr/alarm_control_panel.py
|
||||
@@ -67,11 +69,14 @@ omit =
|
||||
homeassistant/components/arwn/sensor.py
|
||||
homeassistant/components/asterisk_cdr/mailbox.py
|
||||
homeassistant/components/asterisk_mbox/*
|
||||
homeassistant/components/asuswrt/__init__.py
|
||||
homeassistant/components/asuswrt/router.py
|
||||
homeassistant/components/aten_pe/*
|
||||
homeassistant/components/atome/*
|
||||
homeassistant/components/aurora/__init__.py
|
||||
homeassistant/components/aurora/binary_sensor.py
|
||||
homeassistant/components/aurora/const.py
|
||||
homeassistant/components/aurora/sensor.py
|
||||
homeassistant/components/aurora_abb_powerone/sensor.py
|
||||
homeassistant/components/avea/light.py
|
||||
homeassistant/components/avion/light.py
|
||||
@@ -140,6 +145,7 @@ omit =
|
||||
homeassistant/components/clickatell/notify.py
|
||||
homeassistant/components/clicksend/notify.py
|
||||
homeassistant/components/clicksend_tts/notify.py
|
||||
homeassistant/components/climacell/weather.py
|
||||
homeassistant/components/cmus/media_player.py
|
||||
homeassistant/components/co2signal/*
|
||||
homeassistant/components/coinbase/*
|
||||
@@ -156,7 +162,6 @@ omit =
|
||||
homeassistant/components/coolmaster/const.py
|
||||
homeassistant/components/cppm_tracker/device_tracker.py
|
||||
homeassistant/components/cpuspeed/sensor.py
|
||||
homeassistant/components/crimereports/sensor.py
|
||||
homeassistant/components/cups/sensor.py
|
||||
homeassistant/components/currencylayer/sensor.py
|
||||
homeassistant/components/daikin/*
|
||||
@@ -172,7 +177,6 @@ omit =
|
||||
homeassistant/components/denonavr/media_player.py
|
||||
homeassistant/components/denonavr/receiver.py
|
||||
homeassistant/components/deutsche_bahn/sensor.py
|
||||
homeassistant/components/devolo_home_control/__init__.py
|
||||
homeassistant/components/devolo_home_control/binary_sensor.py
|
||||
homeassistant/components/devolo_home_control/climate.py
|
||||
homeassistant/components/devolo_home_control/const.py
|
||||
@@ -268,6 +272,8 @@ omit =
|
||||
homeassistant/components/evohome/*
|
||||
homeassistant/components/ezviz/*
|
||||
homeassistant/components/familyhub/camera.py
|
||||
homeassistant/components/faa_delays/__init__.py
|
||||
homeassistant/components/faa_delays/binary_sensor.py
|
||||
homeassistant/components/fastdotcom/*
|
||||
homeassistant/components/ffmpeg/camera.py
|
||||
homeassistant/components/fibaro/*
|
||||
@@ -358,7 +364,9 @@ omit =
|
||||
homeassistant/components/guardian/sensor.py
|
||||
homeassistant/components/guardian/switch.py
|
||||
homeassistant/components/guardian/util.py
|
||||
homeassistant/components/habitica/*
|
||||
homeassistant/components/habitica/__init__.py
|
||||
homeassistant/components/habitica/const.py
|
||||
homeassistant/components/habitica/sensor.py
|
||||
homeassistant/components/hangouts/*
|
||||
homeassistant/components/hangouts/__init__.py
|
||||
homeassistant/components/hangouts/const.py
|
||||
@@ -463,7 +471,11 @@ omit =
|
||||
homeassistant/components/kaiterra/*
|
||||
homeassistant/components/kankun/switch.py
|
||||
homeassistant/components/keba/*
|
||||
homeassistant/components/keenetic_ndms2/__init__.py
|
||||
homeassistant/components/keenetic_ndms2/binary_sensor.py
|
||||
homeassistant/components/keenetic_ndms2/const.py
|
||||
homeassistant/components/keenetic_ndms2/device_tracker.py
|
||||
homeassistant/components/keenetic_ndms2/router.py
|
||||
homeassistant/components/kef/*
|
||||
homeassistant/components/keyboard/*
|
||||
homeassistant/components/keyboard_remote/*
|
||||
@@ -519,6 +531,10 @@ omit =
|
||||
homeassistant/components/lutron_caseta/switch.py
|
||||
homeassistant/components/lw12wifi/light.py
|
||||
homeassistant/components/lyft/sensor.py
|
||||
homeassistant/components/lyric/__init__.py
|
||||
homeassistant/components/lyric/api.py
|
||||
homeassistant/components/lyric/climate.py
|
||||
homeassistant/components/lyric/sensor.py
|
||||
homeassistant/components/magicseaweed/sensor.py
|
||||
homeassistant/components/mailgun/notify.py
|
||||
homeassistant/components/map/*
|
||||
@@ -560,6 +576,7 @@ omit =
|
||||
homeassistant/components/mochad/*
|
||||
homeassistant/components/modbus/climate.py
|
||||
homeassistant/components/modbus/cover.py
|
||||
homeassistant/components/modbus/modbus.py
|
||||
homeassistant/components/modbus/switch.py
|
||||
homeassistant/components/modbus/sensor.py
|
||||
homeassistant/components/modem_callerid/sensor.py
|
||||
@@ -571,11 +588,27 @@ omit =
|
||||
homeassistant/components/mpd/media_player.py
|
||||
homeassistant/components/mqtt_room/sensor.py
|
||||
homeassistant/components/msteams/notify.py
|
||||
homeassistant/components/mullvad/__init__.py
|
||||
homeassistant/components/mullvad/binary_sensor.py
|
||||
homeassistant/components/nest/const.py
|
||||
homeassistant/components/mvglive/sensor.py
|
||||
homeassistant/components/mychevy/*
|
||||
homeassistant/components/mycroft/*
|
||||
homeassistant/components/mycroft/notify.py
|
||||
homeassistant/components/mysensors/*
|
||||
homeassistant/components/mysensors/__init__.py
|
||||
homeassistant/components/mysensors/binary_sensor.py
|
||||
homeassistant/components/mysensors/climate.py
|
||||
homeassistant/components/mysensors/const.py
|
||||
homeassistant/components/mysensors/cover.py
|
||||
homeassistant/components/mysensors/device.py
|
||||
homeassistant/components/mysensors/device_tracker.py
|
||||
homeassistant/components/mysensors/gateway.py
|
||||
homeassistant/components/mysensors/handler.py
|
||||
homeassistant/components/mysensors/helpers.py
|
||||
homeassistant/components/mysensors/light.py
|
||||
homeassistant/components/mysensors/notify.py
|
||||
homeassistant/components/mysensors/sensor.py
|
||||
homeassistant/components/mysensors/switch.py
|
||||
homeassistant/components/mystrom/binary_sensor.py
|
||||
homeassistant/components/mystrom/light.py
|
||||
homeassistant/components/mystrom/switch.py
|
||||
@@ -621,7 +654,8 @@ omit =
|
||||
homeassistant/components/norway_air/air_quality.py
|
||||
homeassistant/components/notify_events/notify.py
|
||||
homeassistant/components/nsw_fuel_station/sensor.py
|
||||
homeassistant/components/nuimo_controller/*
|
||||
homeassistant/components/nuki/__init__.py
|
||||
homeassistant/components/nuki/const.py
|
||||
homeassistant/components/nuki/lock.py
|
||||
homeassistant/components/nut/sensor.py
|
||||
homeassistant/components/nx584/alarm_control_panel.py
|
||||
@@ -687,6 +721,7 @@ omit =
|
||||
homeassistant/components/pandora/media_player.py
|
||||
homeassistant/components/pcal9535a/*
|
||||
homeassistant/components/pencom/switch.py
|
||||
homeassistant/components/philips_js/__init__.py
|
||||
homeassistant/components/philips_js/media_player.py
|
||||
homeassistant/components/pi_hole/sensor.py
|
||||
homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py
|
||||
@@ -699,7 +734,11 @@ omit =
|
||||
homeassistant/components/ping/device_tracker.py
|
||||
homeassistant/components/pioneer/media_player.py
|
||||
homeassistant/components/pjlink/media_player.py
|
||||
homeassistant/components/plaato/*
|
||||
homeassistant/components/plaato/__init__.py
|
||||
homeassistant/components/plaato/binary_sensor.py
|
||||
homeassistant/components/plaato/const.py
|
||||
homeassistant/components/plaato/entity.py
|
||||
homeassistant/components/plaato/sensor.py
|
||||
homeassistant/components/plex/media_player.py
|
||||
homeassistant/components/plum_lightpad/light.py
|
||||
homeassistant/components/pocketcasts/sensor.py
|
||||
@@ -754,6 +793,8 @@ omit =
|
||||
homeassistant/components/rest/switch.py
|
||||
homeassistant/components/ring/camera.py
|
||||
homeassistant/components/ripple/sensor.py
|
||||
homeassistant/components/rituals_perfume_genie/switch.py
|
||||
homeassistant/components/rituals_perfume_genie/__init__.py
|
||||
homeassistant/components/rocketchat/notify.py
|
||||
homeassistant/components/roomba/binary_sensor.py
|
||||
homeassistant/components/roomba/braava.py
|
||||
@@ -876,7 +917,6 @@ omit =
|
||||
homeassistant/components/switcher_kis/switch.py
|
||||
homeassistant/components/switchmate/switch.py
|
||||
homeassistant/components/syncthru/sensor.py
|
||||
homeassistant/components/synology/camera.py
|
||||
homeassistant/components/synology_chat/notify.py
|
||||
homeassistant/components/synology_dsm/__init__.py
|
||||
homeassistant/components/synology_dsm/binary_sensor.py
|
||||
@@ -944,7 +984,10 @@ omit =
|
||||
homeassistant/components/toon/sensor.py
|
||||
homeassistant/components/toon/switch.py
|
||||
homeassistant/components/torque/sensor.py
|
||||
homeassistant/components/totalconnect/*
|
||||
homeassistant/components/totalconnect/__init__.py
|
||||
homeassistant/components/totalconnect/alarm_control_panel.py
|
||||
homeassistant/components/totalconnect/binary_sensor.py
|
||||
homeassistant/components/totalconnect/const.py
|
||||
homeassistant/components/touchline/climate.py
|
||||
homeassistant/components/tplink/common.py
|
||||
homeassistant/components/tplink/switch.py
|
||||
@@ -963,7 +1006,14 @@ omit =
|
||||
homeassistant/components/transmission/const.py
|
||||
homeassistant/components/transmission/errors.py
|
||||
homeassistant/components/travisci/sensor.py
|
||||
homeassistant/components/tuya/*
|
||||
homeassistant/components/tuya/__init__.py
|
||||
homeassistant/components/tuya/climate.py
|
||||
homeassistant/components/tuya/const.py
|
||||
homeassistant/components/tuya/cover.py
|
||||
homeassistant/components/tuya/fan.py
|
||||
homeassistant/components/tuya/light.py
|
||||
homeassistant/components/tuya/scene.py
|
||||
homeassistant/components/tuya/switch.py
|
||||
homeassistant/components/twentemilieu/const.py
|
||||
homeassistant/components/twentemilieu/sensor.py
|
||||
homeassistant/components/twilio_call/notify.py
|
||||
@@ -1001,6 +1051,7 @@ omit =
|
||||
homeassistant/components/vesync/common.py
|
||||
homeassistant/components/vesync/const.py
|
||||
homeassistant/components/vesync/fan.py
|
||||
homeassistant/components/vesync/light.py
|
||||
homeassistant/components/vesync/switch.py
|
||||
homeassistant/components/viaggiatreno/sensor.py
|
||||
homeassistant/components/vicare/*
|
||||
@@ -1043,7 +1094,6 @@ omit =
|
||||
homeassistant/components/xbox/sensor.py
|
||||
homeassistant/components/xbox_live/sensor.py
|
||||
homeassistant/components/xeoma/camera.py
|
||||
homeassistant/components/xfinity/device_tracker.py
|
||||
homeassistant/components/xiaomi/camera.py
|
||||
homeassistant/components/xiaomi_aqara/__init__.py
|
||||
homeassistant/components/xiaomi_aqara/binary_sensor.py
|
||||
@@ -1056,6 +1106,7 @@ omit =
|
||||
homeassistant/components/xiaomi_miio/__init__.py
|
||||
homeassistant/components/xiaomi_miio/air_quality.py
|
||||
homeassistant/components/xiaomi_miio/alarm_control_panel.py
|
||||
homeassistant/components/xiaomi_miio/device.py
|
||||
homeassistant/components/xiaomi_miio/device_tracker.py
|
||||
homeassistant/components/xiaomi_miio/fan.py
|
||||
homeassistant/components/xiaomi_miio/gateway.py
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
"name": "Home Assistant Dev",
|
||||
"context": "..",
|
||||
"dockerFile": "../Dockerfile.dev",
|
||||
"postCreateCommand": "mkdir -p config && pip3 install -e .",
|
||||
"postCreateCommand": "script/setup",
|
||||
"postStartCommand": "script/bootstrap",
|
||||
"containerEnv": { "DEVCONTAINER": "1" },
|
||||
"appPort": 8123,
|
||||
"runArgs": ["-e", "GIT_EDITOR=code --wait"],
|
||||
"extensions": [
|
||||
"ms-python.vscode-pylance",
|
||||
"visualstudioexptteam.vscodeintellicode",
|
||||
"ms-azure-devops.azure-pipelines",
|
||||
"redhat.vscode-yaml",
|
||||
"esbenp.prettier-vscode"
|
||||
],
|
||||
@@ -19,12 +20,11 @@
|
||||
"python.linting.enabled": true,
|
||||
"python.formatting.provider": "black",
|
||||
"python.testing.pytestArgs": ["--no-cov"],
|
||||
"python.testing.pytestEnabled": true,
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"terminal.integrated.shell.linux": "/bin/bash",
|
||||
"terminal.integrated.shell.linux": "/usr/bin/zsh",
|
||||
"yaml.customTags": [
|
||||
"!input scalar",
|
||||
"!secret scalar",
|
||||
|
||||
53
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
53
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
@@ -1,53 +0,0 @@
|
||||
---
|
||||
name: Report a bug with Home Assistant Core
|
||||
about: Report an issue with Home Assistant Core
|
||||
---
|
||||
<!-- READ THIS FIRST:
|
||||
- If you need additional help with this template, please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/core/releases
|
||||
- Do not report issues for integrations if you are using custom components or integrations.
|
||||
- Provide as many details as possible. Paste logs, configuration samples and code into the backticks.
|
||||
DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed without comment.
|
||||
-->
|
||||
## The problem
|
||||
<!--
|
||||
Describe the issue you are experiencing here to communicate to the
|
||||
maintainers. Tell us what you were trying to do and what happened.
|
||||
-->
|
||||
|
||||
|
||||
## Environment
|
||||
<!--
|
||||
Provide details about the versions you are using, which helps us to reproduce
|
||||
and find the issue quicker. Version information is found in the
|
||||
Home Assistant frontend: Configuration -> Info.
|
||||
-->
|
||||
|
||||
- Home Assistant Core release with the issue:
|
||||
- Last working Home Assistant Core release (if known):
|
||||
- Operating environment (OS/Container/Supervised/Core):
|
||||
- Integration causing this issue:
|
||||
- Link to integration documentation on our website:
|
||||
|
||||
## Problem-relevant `configuration.yaml`
|
||||
<!--
|
||||
An example configuration that caused the problem for you. Fill this out even
|
||||
if it seems unimportant to you. Please be sure to remove personal information
|
||||
like passwords, private URLs and other credentials.
|
||||
-->
|
||||
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
## Traceback/Error logs
|
||||
<!--
|
||||
If you come across any trace or error logs, please provide them.
|
||||
-->
|
||||
|
||||
```txt
|
||||
|
||||
```
|
||||
|
||||
## Additional information
|
||||
|
||||
102
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
102
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
name: Report an issue with Home Assistant Core
|
||||
about: Report an issue with Home Assistant Core.
|
||||
title: ""
|
||||
issue_body: true
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
This issue form is for reporting bugs only!
|
||||
|
||||
If you have a feature or enhancement request, please use the [feature request][fr] section of our [Community Forum][fr].
|
||||
|
||||
[fr]: https://community.home-assistant.io/c/feature-requests
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: The problem
|
||||
description: >-
|
||||
Describe the issue you are experiencing here to communicate to the
|
||||
maintainers. Tell us what you were trying to do and what happened.
|
||||
|
||||
Provide a clear and concise description of what the problem is.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Environment
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: What is version of Home Assistant Core has the issue?
|
||||
placeholder: core-
|
||||
description: >
|
||||
Can be found in the Configuration panel -> Info.
|
||||
- type: input
|
||||
attributes:
|
||||
label: What was the last working version of Home Assistant Core?
|
||||
placeholder: core-
|
||||
description: >
|
||||
If known, otherwise leave blank.
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: What type of installation are you running?
|
||||
description: >
|
||||
If you don't know, you can find it in: Configuration panel -> Info.
|
||||
options:
|
||||
- Home Assistant OS
|
||||
- Home Assistant Container
|
||||
- Home Assistant Supervised
|
||||
- Home Assistant Core
|
||||
- type: input
|
||||
attributes:
|
||||
label: Integration causing the issue
|
||||
description: >
|
||||
The name of the integration, for example, Automation or Philips Hue.
|
||||
- type: input
|
||||
attributes:
|
||||
label: Link to integration documentation on our website
|
||||
placeholder: "https://www.home-assistant.io/integrations/..."
|
||||
description: |
|
||||
Providing a link [to the documentation][docs] help us categorizing the
|
||||
issue, while providing a useful reference at the same time.
|
||||
|
||||
[docs]: https://www.home-assistant.io/integrations
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Details
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Example YAML snippet
|
||||
description: |
|
||||
If this issue has an example piece of YAML that can help reproducing this problem, please provide.
|
||||
This can be an piece of YAML from, e.g., an automation, script, scene or configuration.
|
||||
value: |
|
||||
```yaml
|
||||
# Put your YAML below this line
|
||||
|
||||
```
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything in the logs that might be useful for us?
|
||||
description: For example, error message, or stack traces.
|
||||
value: |
|
||||
```txt
|
||||
# Put your logs below this line
|
||||
|
||||
```
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Additional information
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
If you have any additional information for us, use the field below.
|
||||
Please note, you can attach screenshots or screen recordings here, by
|
||||
dragging and dropping files in the field below.
|
||||
54
.github/workflows/ci.yaml
vendored
54
.github/workflows/ci.yaml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
pip install -r requirements.txt -r requirements_test.txt
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -140,7 +140,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -236,7 +236,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -252,7 +252,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -284,7 +284,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -300,7 +300,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -332,7 +332,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -348,7 +348,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -377,7 +377,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -393,7 +393,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -425,7 +425,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -441,7 +441,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -481,7 +481,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -497,7 +497,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -528,7 +528,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -560,7 +560,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -591,7 +591,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -630,7 +630,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -664,7 +664,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -700,7 +700,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -760,7 +760,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
|
||||
6
.github/workflows/stale.yml
vendored
6
.github/workflows/stale.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
# - No PRs marked as no-stale
|
||||
# - No issues marked as no-stale or help-wanted
|
||||
- name: 90 days stale issues & PRs policy
|
||||
uses: actions/stale@v3.0.15
|
||||
uses: actions/stale@v3.0.17
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
# - No PRs marked as no-stale or new-integrations
|
||||
# - No issues (-1)
|
||||
- name: 30 days stale PRs policy
|
||||
uses: actions/stale@v3.0.15
|
||||
uses: actions/stale@v3.0.17
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 30
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
# - No Issues marked as no-stale or help-wanted
|
||||
# - No PRs (-1)
|
||||
- name: Needs more information stale issues policy
|
||||
uses: actions/stale@v3.0.15
|
||||
uses: actions/stale@v3.0.17
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
only-labels: "needs-more-information"
|
||||
|
||||
@@ -59,8 +59,8 @@ repos:
|
||||
rev: v1.24.2
|
||||
hooks:
|
||||
- id: yamllint
|
||||
- repo: https://github.com/prettier/prettier
|
||||
rev: 2.0.4
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.2.1
|
||||
hooks:
|
||||
- id: prettier
|
||||
stages: [manual]
|
||||
@@ -90,4 +90,4 @@ repos:
|
||||
pass_filenames: false
|
||||
language: script
|
||||
types: [text]
|
||||
files: ^(homeassistant/.+/(manifest|strings)\.json|\.coveragerc)$
|
||||
files: ^(homeassistant/.+/(manifest|strings)\.json|\.coveragerc|homeassistant/.+/services\.yaml)$
|
||||
|
||||
31
CODEOWNERS
31
CODEOWNERS
@@ -24,6 +24,7 @@ homeassistant/components/accuweather/* @bieniu
|
||||
homeassistant/components/acmeda/* @atmurray
|
||||
homeassistant/components/adguard/* @frenck
|
||||
homeassistant/components/advantage_air/* @Bre77
|
||||
homeassistant/components/aemet/* @noltari
|
||||
homeassistant/components/agent_dvr/* @ispysoftware
|
||||
homeassistant/components/airly/* @bieniu
|
||||
homeassistant/components/airnow/* @asymworks
|
||||
@@ -82,6 +83,7 @@ homeassistant/components/circuit/* @braam
|
||||
homeassistant/components/cisco_ios/* @fbradyirl
|
||||
homeassistant/components/cisco_mobility_express/* @fbradyirl
|
||||
homeassistant/components/cisco_webex_teams/* @fbradyirl
|
||||
homeassistant/components/climacell/* @raman325
|
||||
homeassistant/components/cloud/* @home-assistant/cloud
|
||||
homeassistant/components/cloudflare/* @ludeeus @ctalkington
|
||||
homeassistant/components/color_extractor/* @GenericStudent
|
||||
@@ -144,6 +146,7 @@ homeassistant/components/esphome/* @OttoWinter
|
||||
homeassistant/components/essent/* @TheLastProject
|
||||
homeassistant/components/evohome/* @zxdavb
|
||||
homeassistant/components/ezviz/* @baqs
|
||||
homeassistant/components/faa_delays/* @ntilley905
|
||||
homeassistant/components/fastdotcom/* @rohankapoorcom
|
||||
homeassistant/components/file/* @fabaff
|
||||
homeassistant/components/filter/* @dgomes
|
||||
@@ -158,7 +161,7 @@ homeassistant/components/flunearyou/* @bachya
|
||||
homeassistant/components/forked_daapd/* @uvjustin
|
||||
homeassistant/components/fortios/* @kimfrellsen
|
||||
homeassistant/components/foscam/* @skgsergio
|
||||
homeassistant/components/freebox/* @snoof85 @Quentame
|
||||
homeassistant/components/freebox/* @hacf-fr @Quentame
|
||||
homeassistant/components/fronius/* @nielstron
|
||||
homeassistant/components/frontend/* @home-assistant/frontend
|
||||
homeassistant/components/garmin_connect/* @cyberjunky
|
||||
@@ -181,6 +184,7 @@ homeassistant/components/griddy/* @bdraco
|
||||
homeassistant/components/group/* @home-assistant/core
|
||||
homeassistant/components/growatt_server/* @indykoning
|
||||
homeassistant/components/guardian/* @bachya
|
||||
homeassistant/components/habitica/* @ASMfreaK @leikoilja
|
||||
homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey
|
||||
homeassistant/components/hassio/* @home-assistant/supervisor
|
||||
homeassistant/components/hdmi_cec/* @newAM
|
||||
@@ -241,6 +245,7 @@ homeassistant/components/keba/* @dannerph
|
||||
homeassistant/components/keenetic_ndms2/* @foxel
|
||||
homeassistant/components/kef/* @basnijholt
|
||||
homeassistant/components/keyboard_remote/* @bendavid
|
||||
homeassistant/components/kmtronic/* @dgomes
|
||||
homeassistant/components/knx/* @Julius2342 @farmio @marvin-w
|
||||
homeassistant/components/kodi/* @OnFreund @cgtobi
|
||||
homeassistant/components/konnected/* @heythisisnate @kit-klein
|
||||
@@ -250,6 +255,8 @@ homeassistant/components/launch_library/* @ludeeus
|
||||
homeassistant/components/lcn/* @alengwenus
|
||||
homeassistant/components/life360/* @pnbruckner
|
||||
homeassistant/components/linux_battery/* @fabaff
|
||||
homeassistant/components/litejet/* @joncar
|
||||
homeassistant/components/litterrobot/* @natekspencer
|
||||
homeassistant/components/local_ip/* @issacg
|
||||
homeassistant/components/logger/* @home-assistant/core
|
||||
homeassistant/components/logi_circle/* @evanjd
|
||||
@@ -260,8 +267,10 @@ homeassistant/components/luftdaten/* @fabaff
|
||||
homeassistant/components/lupusec/* @majuss
|
||||
homeassistant/components/lutron/* @JonGilmore
|
||||
homeassistant/components/lutron_caseta/* @swails @bdraco
|
||||
homeassistant/components/lyric/* @timmo001
|
||||
homeassistant/components/mastodon/* @fabaff
|
||||
homeassistant/components/matrix/* @tinloaf
|
||||
homeassistant/components/mazda/* @bdr99
|
||||
homeassistant/components/mcp23017/* @jardiamj
|
||||
homeassistant/components/media_source/* @hunterjm
|
||||
homeassistant/components/mediaroom/* @dgomes
|
||||
@@ -283,10 +292,12 @@ homeassistant/components/monoprice/* @etsinko @OnFreund
|
||||
homeassistant/components/moon/* @fabaff
|
||||
homeassistant/components/motion_blinds/* @starkillerOG
|
||||
homeassistant/components/mpd/* @fabaff
|
||||
homeassistant/components/mqtt/* @home-assistant/core @emontnemery
|
||||
homeassistant/components/mqtt/* @emontnemery
|
||||
homeassistant/components/msteams/* @peroyvind
|
||||
homeassistant/components/mullvad/* @meichthys
|
||||
homeassistant/components/my/* @home-assistant/core
|
||||
homeassistant/components/myq/* @bdraco
|
||||
homeassistant/components/mysensors/* @MartinHjelmare
|
||||
homeassistant/components/mysensors/* @MartinHjelmare @functionpointer
|
||||
homeassistant/components/mystrom/* @fabaff
|
||||
homeassistant/components/neato/* @dshokouhi @Santobert
|
||||
homeassistant/components/nederlandse_spoorwegen/* @YarmoM
|
||||
@@ -310,7 +321,7 @@ homeassistant/components/notion/* @bachya
|
||||
homeassistant/components/nsw_fuel_station/* @nickw444
|
||||
homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte
|
||||
homeassistant/components/nuheat/* @bdraco
|
||||
homeassistant/components/nuki/* @pschmitt @pvizeli
|
||||
homeassistant/components/nuki/* @pschmitt @pvizeli @pree
|
||||
homeassistant/components/numato/* @clssn
|
||||
homeassistant/components/number/* @home-assistant/core @Shulyaka
|
||||
homeassistant/components/nut/* @bdraco
|
||||
@@ -374,9 +385,11 @@ homeassistant/components/random/* @fabaff
|
||||
homeassistant/components/recollect_waste/* @bachya
|
||||
homeassistant/components/rejseplanen/* @DarkFox
|
||||
homeassistant/components/repetier/* @MTrab
|
||||
homeassistant/components/rflink/* @javicalle
|
||||
homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221
|
||||
homeassistant/components/ring/* @balloob
|
||||
homeassistant/components/risco/* @OnFreund
|
||||
homeassistant/components/rituals_perfume_genie/* @milanmeu
|
||||
homeassistant/components/rmvtransport/* @cgtobi
|
||||
homeassistant/components/roku/* @ctalkington
|
||||
homeassistant/components/roomba/* @pschmitt @cyr-ius @shenxn
|
||||
@@ -416,6 +429,7 @@ homeassistant/components/smappee/* @bsmappee
|
||||
homeassistant/components/smart_meter_texas/* @grahamwetzler
|
||||
homeassistant/components/smarthab/* @outadoc
|
||||
homeassistant/components/smartthings/* @andrewsayre
|
||||
homeassistant/components/smarttub/* @mdz
|
||||
homeassistant/components/smarty/* @z0mbieprocess
|
||||
homeassistant/components/sms/* @ocalvo
|
||||
homeassistant/components/smtp/* @fabaff
|
||||
@@ -442,6 +456,7 @@ homeassistant/components/stiebel_eltron/* @fucm
|
||||
homeassistant/components/stookalert/* @fwestenberg
|
||||
homeassistant/components/stream/* @hunterjm @uvjustin
|
||||
homeassistant/components/stt/* @pvizeli
|
||||
homeassistant/components/subaru/* @G-Two
|
||||
homeassistant/components/suez_water/* @ooii
|
||||
homeassistant/components/sun/* @Swamp-Ig
|
||||
homeassistant/components/supla/* @mwegrzynek
|
||||
@@ -455,7 +470,7 @@ homeassistant/components/syncthru/* @nielstron
|
||||
homeassistant/components/synology_dsm/* @hacf-fr @Quentame @mib1185
|
||||
homeassistant/components/synology_srm/* @aerialls
|
||||
homeassistant/components/syslog/* @fabaff
|
||||
homeassistant/components/tado/* @michaelarnauts @bdraco
|
||||
homeassistant/components/tado/* @michaelarnauts @bdraco @noltari
|
||||
homeassistant/components/tag/* @balloob @dmulcahey
|
||||
homeassistant/components/tahoma/* @philklei
|
||||
homeassistant/components/tankerkoenig/* @guillempages
|
||||
@@ -477,7 +492,6 @@ homeassistant/components/toon/* @frenck
|
||||
homeassistant/components/totalconnect/* @austinmroczek
|
||||
homeassistant/components/tplink/* @rytilahti @thegardenmonkey
|
||||
homeassistant/components/traccar/* @ludeeus
|
||||
homeassistant/components/tradfri/* @ggravlingen
|
||||
homeassistant/components/trafikverket_train/* @endor-force
|
||||
homeassistant/components/trafikverket_weatherstation/* @endor-force
|
||||
homeassistant/components/transmission/* @engrbm87 @JPHutchins
|
||||
@@ -485,6 +499,7 @@ homeassistant/components/tts/* @pvizeli
|
||||
homeassistant/components/tuya/* @ollo69
|
||||
homeassistant/components/twentemilieu/* @frenck
|
||||
homeassistant/components/twinkly/* @dr1rrb
|
||||
homeassistant/components/ubus/* @noltari
|
||||
homeassistant/components/unifi/* @Kane610
|
||||
homeassistant/components/unifiled/* @florisvdk
|
||||
homeassistant/components/upb/* @gwww
|
||||
@@ -506,7 +521,7 @@ homeassistant/components/vicare/* @oischinger
|
||||
homeassistant/components/vilfo/* @ManneW
|
||||
homeassistant/components/vivotek/* @HarlemSquirrel
|
||||
homeassistant/components/vizio/* @raman325
|
||||
homeassistant/components/vlc_telnet/* @rodripf
|
||||
homeassistant/components/vlc_telnet/* @rodripf @dmcc
|
||||
homeassistant/components/volkszaehler/* @fabaff
|
||||
homeassistant/components/volumio/* @OnFreund
|
||||
homeassistant/components/waqi/* @andrey-git
|
||||
@@ -514,6 +529,7 @@ homeassistant/components/watson_tts/* @rutkai
|
||||
homeassistant/components/weather/* @fabaff
|
||||
homeassistant/components/webostv/* @bendavid
|
||||
homeassistant/components/websocket_api/* @home-assistant/core
|
||||
homeassistant/components/wemo/* @esev
|
||||
homeassistant/components/wiffi/* @mampfes
|
||||
homeassistant/components/wilight/* @leofig-rj
|
||||
homeassistant/components/withings/* @vangorra
|
||||
@@ -523,7 +539,6 @@ homeassistant/components/workday/* @fabaff
|
||||
homeassistant/components/worldclock/* @fabaff
|
||||
homeassistant/components/xbox/* @hunterjm
|
||||
homeassistant/components/xbox_live/* @MartinHjelmare
|
||||
homeassistant/components/xfinity/* @cisasteelersfan
|
||||
homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
|
||||
homeassistant/components/xiaomi_miio/* @rytilahti @syssi @starkillerOG
|
||||
homeassistant/components/xiaomi_tv/* @simse
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.8
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
||||
&& apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
libudev-dev \
|
||||
libavformat-dev \
|
||||
libavcodec-dev \
|
||||
@@ -10,6 +14,7 @@ RUN \
|
||||
libswscale-dev \
|
||||
libswresample-dev \
|
||||
libavfilter-dev \
|
||||
libpcap-dev \
|
||||
git \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -114,10 +114,12 @@ stages:
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
strategy:
|
||||
maxParallel: 15
|
||||
maxParallel: 17
|
||||
matrix:
|
||||
qemux86-64:
|
||||
buildMachine: 'qemux86-64'
|
||||
generic-x86-64:
|
||||
buildMachine: 'generic-x86-64'
|
||||
intel-nuc:
|
||||
buildMachine: 'intel-nuc'
|
||||
qemux86:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Provide an authentication layer for Home Assistant."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
@@ -24,11 +26,19 @@ _ProviderKey = Tuple[str, Optional[str]]
|
||||
_ProviderDict = Dict[_ProviderKey, AuthProvider]
|
||||
|
||||
|
||||
class InvalidAuthError(Exception):
|
||||
"""Raised when a authentication error occurs."""
|
||||
|
||||
|
||||
class InvalidProvider(Exception):
|
||||
"""Authentication provider not found."""
|
||||
|
||||
|
||||
async def auth_manager_from_config(
|
||||
hass: HomeAssistant,
|
||||
provider_configs: List[Dict[str, Any]],
|
||||
module_configs: List[Dict[str, Any]],
|
||||
) -> "AuthManager":
|
||||
) -> AuthManager:
|
||||
"""Initialize an auth manager from config.
|
||||
|
||||
CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or
|
||||
@@ -68,7 +78,7 @@ async def auth_manager_from_config(
|
||||
class AuthManagerFlowManager(data_entry_flow.FlowManager):
|
||||
"""Manage authentication flows."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, auth_manager: "AuthManager"):
|
||||
def __init__(self, hass: HomeAssistant, auth_manager: AuthManager):
|
||||
"""Init auth manager flows."""
|
||||
super().__init__(hass)
|
||||
self.auth_manager = auth_manager
|
||||
@@ -96,7 +106,7 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager):
|
||||
return result
|
||||
|
||||
# we got final result
|
||||
if isinstance(result["data"], models.User):
|
||||
if isinstance(result["data"], models.Credentials):
|
||||
result["result"] = result["data"]
|
||||
return result
|
||||
|
||||
@@ -120,11 +130,12 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager):
|
||||
modules = await self.auth_manager.async_get_enabled_mfa(user)
|
||||
|
||||
if modules:
|
||||
flow.credential = credentials
|
||||
flow.user = user
|
||||
flow.available_mfa_modules = modules
|
||||
return await flow.async_step_select_mfa_module()
|
||||
|
||||
result["result"] = await self.auth_manager.async_get_or_create_user(credentials)
|
||||
result["result"] = credentials
|
||||
return result
|
||||
|
||||
|
||||
@@ -156,7 +167,7 @@ class AuthManager:
|
||||
return list(self._mfa_modules.values())
|
||||
|
||||
def get_auth_provider(
|
||||
self, provider_type: str, provider_id: str
|
||||
self, provider_type: str, provider_id: Optional[str]
|
||||
) -> Optional[AuthProvider]:
|
||||
"""Return an auth provider, None if not found."""
|
||||
return self._providers.get((provider_type, provider_id))
|
||||
@@ -367,6 +378,7 @@ class AuthManager:
|
||||
client_icon: Optional[str] = None,
|
||||
token_type: Optional[str] = None,
|
||||
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
|
||||
credential: Optional[models.Credentials] = None,
|
||||
) -> models.RefreshToken:
|
||||
"""Create a new refresh token for a user."""
|
||||
if not user.is_active:
|
||||
@@ -415,6 +427,7 @@ class AuthManager:
|
||||
client_icon,
|
||||
token_type,
|
||||
access_token_expiration,
|
||||
credential,
|
||||
)
|
||||
|
||||
async def async_get_refresh_token(
|
||||
@@ -440,6 +453,8 @@ class AuthManager:
|
||||
self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
|
||||
) -> str:
|
||||
"""Create a new access token."""
|
||||
self.async_validate_refresh_token(refresh_token, remote_ip)
|
||||
|
||||
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
|
||||
|
||||
now = dt_util.utcnow()
|
||||
@@ -453,6 +468,40 @@ class AuthManager:
|
||||
algorithm="HS256",
|
||||
).decode()
|
||||
|
||||
@callback
|
||||
def _async_resolve_provider(
|
||||
self, refresh_token: models.RefreshToken
|
||||
) -> Optional[AuthProvider]:
|
||||
"""Get the auth provider for the given refresh token.
|
||||
|
||||
Raises an exception if the expected provider is no longer available or return
|
||||
None if no provider was expected for this refresh token.
|
||||
"""
|
||||
if refresh_token.credential is None:
|
||||
return None
|
||||
|
||||
provider = self.get_auth_provider(
|
||||
refresh_token.credential.auth_provider_type,
|
||||
refresh_token.credential.auth_provider_id,
|
||||
)
|
||||
if provider is None:
|
||||
raise InvalidProvider(
|
||||
f"Auth provider {refresh_token.credential.auth_provider_type}, {refresh_token.credential.auth_provider_id} not available"
|
||||
)
|
||||
return provider
|
||||
|
||||
@callback
|
||||
def async_validate_refresh_token(
|
||||
self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
|
||||
) -> None:
|
||||
"""Validate that a refresh token is usable.
|
||||
|
||||
Will raise InvalidAuthError on errors.
|
||||
"""
|
||||
provider = self._async_resolve_provider(refresh_token)
|
||||
if provider:
|
||||
provider.async_validate_refresh_token(refresh_token, remote_ip)
|
||||
|
||||
async def async_validate_access_token(
|
||||
self, token: str
|
||||
) -> Optional[models.RefreshToken]:
|
||||
|
||||
@@ -208,6 +208,7 @@ class AuthStore:
|
||||
client_icon: Optional[str] = None,
|
||||
token_type: str = models.TOKEN_TYPE_NORMAL,
|
||||
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
|
||||
credential: Optional[models.Credentials] = None,
|
||||
) -> models.RefreshToken:
|
||||
"""Create a new token for a user."""
|
||||
kwargs: Dict[str, Any] = {
|
||||
@@ -215,6 +216,7 @@ class AuthStore:
|
||||
"client_id": client_id,
|
||||
"token_type": token_type,
|
||||
"access_token_expiration": access_token_expiration,
|
||||
"credential": credential,
|
||||
}
|
||||
if client_name:
|
||||
kwargs["client_name"] = client_name
|
||||
@@ -309,6 +311,7 @@ class AuthStore:
|
||||
|
||||
users: Dict[str, models.User] = OrderedDict()
|
||||
groups: Dict[str, models.Group] = OrderedDict()
|
||||
credentials: Dict[str, models.Credentials] = OrderedDict()
|
||||
|
||||
# Soft-migrating data as we load. We are going to make sure we have a
|
||||
# read only group and an admin group. There are two states that we can
|
||||
@@ -415,15 +418,15 @@ class AuthStore:
|
||||
)
|
||||
|
||||
for cred_dict in data["credentials"]:
|
||||
users[cred_dict["user_id"]].credentials.append(
|
||||
models.Credentials(
|
||||
id=cred_dict["id"],
|
||||
is_new=False,
|
||||
auth_provider_type=cred_dict["auth_provider_type"],
|
||||
auth_provider_id=cred_dict["auth_provider_id"],
|
||||
data=cred_dict["data"],
|
||||
)
|
||||
credential = models.Credentials(
|
||||
id=cred_dict["id"],
|
||||
is_new=False,
|
||||
auth_provider_type=cred_dict["auth_provider_type"],
|
||||
auth_provider_id=cred_dict["auth_provider_id"],
|
||||
data=cred_dict["data"],
|
||||
)
|
||||
credentials[cred_dict["id"]] = credential
|
||||
users[cred_dict["user_id"]].credentials.append(credential)
|
||||
|
||||
for rt_dict in data["refresh_tokens"]:
|
||||
# Filter out the old keys that don't have jwt_key (pre-0.76)
|
||||
@@ -469,6 +472,8 @@ class AuthStore:
|
||||
jwt_key=rt_dict["jwt_key"],
|
||||
last_used_at=last_used_at,
|
||||
last_used_ip=rt_dict.get("last_used_ip"),
|
||||
credential=credentials.get(rt_dict.get("credential_id")),
|
||||
version=rt_dict.get("version"),
|
||||
)
|
||||
users[rt_dict["user_id"]].refresh_tokens[token.id] = token
|
||||
|
||||
@@ -542,6 +547,10 @@ class AuthStore:
|
||||
if refresh_token.last_used_at
|
||||
else None,
|
||||
"last_used_ip": refresh_token.last_used_ip,
|
||||
"credential_id": refresh_token.credential.id
|
||||
if refresh_token.credential
|
||||
else None,
|
||||
"version": refresh_token.version,
|
||||
}
|
||||
for user in self._users.values()
|
||||
for refresh_token in user.refresh_tokens.values()
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Pluggable auth modules for Home Assistant."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import types
|
||||
@@ -66,7 +68,7 @@ class MultiFactorAuthModule:
|
||||
"""Return a voluptuous schema to define mfa auth module's input."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_setup_flow(self, user_id: str) -> "SetupFlow":
|
||||
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
||||
"""Return a data entry flow handler for setup module.
|
||||
|
||||
Mfa module should extend SetupFlow
|
||||
|
||||
@@ -198,7 +198,7 @@ class TotpSetupFlow(SetupFlow):
|
||||
errors: Dict[str, str] = {}
|
||||
|
||||
if user_input:
|
||||
verified = await self.hass.async_add_executor_job( # type: ignore
|
||||
verified = await self.hass.async_add_executor_job(
|
||||
pyotp.TOTP(self._ota_secret).verify, user_input["code"]
|
||||
)
|
||||
if verified:
|
||||
|
||||
@@ -6,6 +6,7 @@ import uuid
|
||||
|
||||
import attr
|
||||
|
||||
from homeassistant.const import __version__
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import permissions as perm_mdl
|
||||
@@ -106,6 +107,10 @@ class RefreshToken:
|
||||
last_used_at: Optional[datetime] = attr.ib(default=None)
|
||||
last_used_ip: Optional[str] = attr.ib(default=None)
|
||||
|
||||
credential: Optional["Credentials"] = attr.ib(default=None)
|
||||
|
||||
version: Optional[str] = attr.ib(default=__version__)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class Credentials:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Auth providers for Home Assistant."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import types
|
||||
@@ -16,7 +18,7 @@ from homeassistant.util.decorator import Registry
|
||||
|
||||
from ..auth_store import AuthStore
|
||||
from ..const import MFA_SESSION_EXPIRATION
|
||||
from ..models import Credentials, User, UserMeta
|
||||
from ..models import Credentials, RefreshToken, User, UserMeta
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_REQS = "auth_prov_reqs_processed"
|
||||
@@ -92,7 +94,7 @@ class AuthProvider:
|
||||
|
||||
# Implement by extending class
|
||||
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> "LoginFlow":
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
||||
"""Return the data flow for logging in with auth provider.
|
||||
|
||||
Auth provider should extend LoginFlow and return an instance.
|
||||
@@ -117,6 +119,16 @@ class AuthProvider:
|
||||
async def async_initialize(self) -> None:
|
||||
"""Initialize the auth provider."""
|
||||
|
||||
@callback
|
||||
def async_validate_refresh_token(
|
||||
self, refresh_token: RefreshToken, remote_ip: Optional[str] = None
|
||||
) -> None:
|
||||
"""Verify a refresh token is still valid.
|
||||
|
||||
Optional hook for an auth provider to verify validity of a refresh token.
|
||||
Should raise InvalidAuthError on errors.
|
||||
"""
|
||||
|
||||
|
||||
async def auth_provider_from_config(
|
||||
hass: HomeAssistant, store: AuthStore, config: Dict[str, Any]
|
||||
@@ -182,6 +194,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
self.created_at = dt_util.utcnow()
|
||||
self.invalid_mfa_times = 0
|
||||
self.user: Optional[User] = None
|
||||
self.credential: Optional[Credentials] = None
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
@@ -222,6 +235,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle the step of mfa validation."""
|
||||
assert self.credential
|
||||
assert self.user
|
||||
|
||||
errors = {}
|
||||
@@ -257,7 +271,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
return self.async_abort(reason="too_many_retry")
|
||||
|
||||
if not errors:
|
||||
return await self.async_finish(self.user)
|
||||
return await self.async_finish(self.credential)
|
||||
|
||||
description_placeholders: Dict[str, Optional[str]] = {
|
||||
"mfa_module_name": auth_module.name,
|
||||
|
||||
@@ -8,12 +8,12 @@ from typing import Any, Dict, Optional, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_COMMAND
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||
from ..models import Credentials, UserMeta
|
||||
|
||||
CONF_COMMAND = "command"
|
||||
CONF_ARGS = "args"
|
||||
CONF_META = "meta"
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Home Assistant auth provider."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
from collections import OrderedDict
|
||||
@@ -31,7 +33,7 @@ CONFIG_SCHEMA = vol.All(AUTH_PROVIDER_SCHEMA, _disallow_id)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_provider(hass: HomeAssistant) -> "HassAuthProvider":
|
||||
def async_get_provider(hass: HomeAssistant) -> HassAuthProvider:
|
||||
"""Get the provider."""
|
||||
for prv in hass.auth.auth_providers:
|
||||
if prv.type == "homeassistant":
|
||||
|
||||
@@ -8,13 +8,12 @@ from typing import Any, Dict, Optional, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||
from .. import AuthManager
|
||||
from ..models import Credentials, User, UserMeta
|
||||
from ..models import Credentials, UserMeta
|
||||
|
||||
AUTH_PROVIDER_TYPE = "legacy_api_password"
|
||||
CONF_API_PASSWORD = "api_password"
|
||||
@@ -30,23 +29,6 @@ class InvalidAuthError(HomeAssistantError):
|
||||
"""Raised when submitting invalid authentication."""
|
||||
|
||||
|
||||
async def async_validate_password(hass: HomeAssistant, password: str) -> Optional[User]:
|
||||
"""Return a user if password is valid. None if not."""
|
||||
auth = cast(AuthManager, hass.auth) # type: ignore
|
||||
providers = auth.get_auth_providers(AUTH_PROVIDER_TYPE)
|
||||
if not providers:
|
||||
raise ValueError("Legacy API password provider not found")
|
||||
|
||||
try:
|
||||
provider = cast(LegacyApiPasswordAuthProvider, providers[0])
|
||||
provider.async_validate_login(password)
|
||||
return await auth.async_get_or_create_user(
|
||||
await provider.async_get_or_create_credentials({})
|
||||
)
|
||||
except InvalidAuthError:
|
||||
return None
|
||||
|
||||
|
||||
@AUTH_PROVIDERS.register(AUTH_PROVIDER_TYPE)
|
||||
class LegacyApiPasswordAuthProvider(AuthProvider):
|
||||
"""An auth provider support legacy api_password."""
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
It shows list of users if access from trusted network.
|
||||
Abort login flow if not access from trusted network.
|
||||
"""
|
||||
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_network
|
||||
from ipaddress import (
|
||||
IPv4Address,
|
||||
IPv4Network,
|
||||
IPv6Address,
|
||||
IPv6Network,
|
||||
ip_address,
|
||||
ip_network,
|
||||
)
|
||||
from typing import Any, Dict, List, Optional, Union, cast
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -13,7 +20,8 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||
from ..models import Credentials, UserMeta
|
||||
from .. import InvalidAuthError
|
||||
from ..models import Credentials, RefreshToken, UserMeta
|
||||
|
||||
IPAddress = Union[IPv4Address, IPv6Address]
|
||||
IPNetwork = Union[IPv4Network, IPv6Network]
|
||||
@@ -46,10 +54,6 @@ CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
class InvalidAuthError(HomeAssistantError):
|
||||
"""Raised when try to access from untrusted networks."""
|
||||
|
||||
|
||||
class InvalidUserError(HomeAssistantError):
|
||||
"""Raised when try to login as invalid user."""
|
||||
|
||||
@@ -163,6 +167,17 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||
):
|
||||
raise InvalidAuthError("Not in trusted_networks")
|
||||
|
||||
@callback
|
||||
def async_validate_refresh_token(
|
||||
self, refresh_token: RefreshToken, remote_ip: Optional[str] = None
|
||||
) -> None:
|
||||
"""Verify a refresh token is still valid."""
|
||||
if remote_ip is None:
|
||||
raise InvalidAuthError(
|
||||
"Unknown remote ip can't be used for trusted network provider."
|
||||
)
|
||||
self.async_validate_access(ip_address(remote_ip))
|
||||
|
||||
|
||||
class TrustedNetworksLoginFlow(LoginFlow):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant import config as conf_util, config_entries, core, loader
|
||||
from homeassistant.components import http
|
||||
from homeassistant.const import REQUIRED_NEXT_PYTHON_DATE, REQUIRED_NEXT_PYTHON_VER
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import area_registry, device_registry, entity_registry
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.setup import (
|
||||
DATA_SETUP,
|
||||
@@ -510,10 +511,12 @@ async def _async_set_up_integrations(
|
||||
|
||||
stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains
|
||||
|
||||
# Kick off loading the registries. They don't need to be awaited.
|
||||
asyncio.create_task(hass.helpers.device_registry.async_get_registry())
|
||||
asyncio.create_task(hass.helpers.entity_registry.async_get_registry())
|
||||
asyncio.create_task(hass.helpers.area_registry.async_get_registry())
|
||||
# Load the registries
|
||||
await asyncio.gather(
|
||||
device_registry.async_load(hass),
|
||||
entity_registry.async_load(hass),
|
||||
area_registry.async_load(hass),
|
||||
)
|
||||
|
||||
# Start setup
|
||||
if stage_1_domains:
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_DATE,
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_TIME,
|
||||
CONF_PASSWORD,
|
||||
@@ -32,7 +33,6 @@ SERVICE_SETTINGS = "change_setting"
|
||||
SERVICE_CAPTURE_IMAGE = "capture_image"
|
||||
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
|
||||
|
||||
ATTR_DEVICE_ID = "device_id"
|
||||
ATTR_DEVICE_NAME = "device_name"
|
||||
ATTR_DEVICE_TYPE = "device_type"
|
||||
ATTR_EVENT_CODE = "event_code"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "La riautenticazione ha avuto successo",
|
||||
"reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente",
|
||||
"single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "\ud558\ub098\uc758 Abode \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
|
||||
"reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4",
|
||||
"single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
|
||||
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "\ube44\ubc00\ubc88\ud638",
|
||||
"username": "\uc774\uba54\uc77c"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "\ube44\ubc00\ubc88\ud638",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "Herauthenticatie was succesvol",
|
||||
"single_instance_allowed": "Slechts een enkele configuratie van Abode is toegestaan."
|
||||
},
|
||||
"error": {
|
||||
@@ -12,9 +13,14 @@
|
||||
"mfa": {
|
||||
"data": {
|
||||
"mfa_code": "MFA-code (6-cijfers)"
|
||||
}
|
||||
},
|
||||
"title": "Voer uw MFA-code voor Abode in"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "Wachtwoord",
|
||||
"username": "E-mail"
|
||||
},
|
||||
"title": "Vul uw Abode-inloggegevens in"
|
||||
},
|
||||
"user": {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
|
||||
"invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
|
||||
"invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
|
||||
"invalid_mfa_code": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434 MFA."
|
||||
},
|
||||
"step": {
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.components.weather import (
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ICON,
|
||||
CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
LENGTH_FEET,
|
||||
@@ -33,7 +34,6 @@ from homeassistant.const import (
|
||||
)
|
||||
|
||||
ATTRIBUTION = "Data provided by AccuWeather"
|
||||
ATTR_ICON = "icon"
|
||||
ATTR_FORECAST = CONF_FORECAST = "forecast"
|
||||
ATTR_LABEL = "label"
|
||||
ATTR_UNIT_IMPERIAL = "Imperial"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "accuweather",
|
||||
"name": "AccuWeather",
|
||||
"documentation": "https://www.home-assistant.io/integrations/accuweather/",
|
||||
"requirements": ["accuweather==0.0.11"],
|
||||
"requirements": ["accuweather==0.1.0"],
|
||||
"codeowners": ["@bieniu"],
|
||||
"config_flow": true,
|
||||
"quality_scale": "platinum"
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
|
||||
"invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API \ud0a4",
|
||||
"latitude": "\uc704\ub3c4",
|
||||
"longitude": "\uacbd\ub3c4",
|
||||
"name": "\uc774\ub984"
|
||||
},
|
||||
"description": "\uad6c\uc131\uc5d0 \ub300\ud55c \ub3c4\uc6c0\uc774 \ud544\uc694\ud55c \uacbd\uc6b0 \ub2e4\uc74c\uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694:\nhttps://www.home-assistant.io/integrations/accuweather/\n\n\uc77c\ubd80 \uc13c\uc11c\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc5f0\ub3d9 \uad6c\uc131 \ud6c4 \uad6c\uc131\uc694\uc18c \ub808\uc9c0\uc2a4\ud2b8\ub9ac\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n\uc77c\uae30\uc608\ubcf4\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc5f0\ub3d9 \uc635\uc158\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "\ubb34\ub8cc \ubc84\uc804\uc758 AccuWeather API \ud0a4\ub85c \uc77c\uae30\uc608\ubcf4\ub97c \ud65c\uc131\ud654\ud55c \uacbd\uc6b0 \uc81c\ud55c\uc0ac\ud56d\uc73c\ub85c \uc778\ud574 \uc5c5\ub370\uc774\ud2b8\ub294 40 \ubd84\uc774 \uc544\ub2cc 80 \ubd84\ub9c8\ub2e4 \uc218\ud589\ub429\ub2c8\ub2e4."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
|
||||
from homeassistant.const import (
|
||||
CONF_FILENAME,
|
||||
CONF_NAME,
|
||||
CONF_TIMEOUT,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNKNOWN,
|
||||
@@ -17,7 +18,6 @@ import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_TIMEOUT = "timeout"
|
||||
CONF_WRITE_TIMEOUT = "write_timeout"
|
||||
|
||||
DEFAULT_NAME = "Acer Projector"
|
||||
@@ -74,7 +74,6 @@ class AcerSwitch(SwitchEntity):
|
||||
|
||||
def __init__(self, serial_port, name, timeout, write_timeout, **kwargs):
|
||||
"""Init of the Acer projector."""
|
||||
|
||||
self.ser = serial.Serial(
|
||||
port=serial_port, timeout=timeout, write_timeout=write_timeout, **kwargs
|
||||
)
|
||||
@@ -90,7 +89,6 @@ class AcerSwitch(SwitchEntity):
|
||||
|
||||
def _write_read(self, msg):
|
||||
"""Write to the projector and read the return."""
|
||||
|
||||
ret = ""
|
||||
# Sometimes the projector won't answer for no reason or the projector
|
||||
# was disconnected during runtime.
|
||||
|
||||
@@ -32,7 +32,7 @@ class AcmedaBase(entity.Entity):
|
||||
device.id, remove_config_entry_id=self.registry_entry.config_entry_id
|
||||
)
|
||||
|
||||
await self.async_remove()
|
||||
await self.async_remove(force_remove=True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Entity has been added to hass."""
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Geen apparaten gevonden op het netwerk"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.",
|
||||
"single_instance_allowed": "\ud558\ub098\uc758 AdGuard Home \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
|
||||
"single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
@@ -14,9 +17,9 @@
|
||||
"host": "\ud638\uc2a4\ud2b8",
|
||||
"password": "\ube44\ubc00\ubc88\ud638",
|
||||
"port": "\ud3ec\ud2b8",
|
||||
"ssl": "AdGuard Home \uc740 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4",
|
||||
"ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9",
|
||||
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984",
|
||||
"verify_ssl": "AdGuard Home \uc740 \uc62c\ubc14\ub978 \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4"
|
||||
"verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778"
|
||||
},
|
||||
"description": "\ubaa8\ub2c8\ud130\ub9c1 \ubc0f \uc81c\uc5b4\uac00 \uac00\ub2a5\ud558\ub3c4\ub85d AdGuard Home \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694."
|
||||
}
|
||||
|
||||
18
homeassistant/components/advantage_air/translations/ko.json
Normal file
18
homeassistant/components/advantage_air/translations/ko.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"ip_address": "IP \uc8fc\uc18c",
|
||||
"port": "\ud3ec\ud2b8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,13 @@
|
||||
"abort": {
|
||||
"already_configured": "Apparaat is al geconfigureerd"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Kan geen verbinding maken"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"ip_address": "IP-adres",
|
||||
"port": "Poort"
|
||||
},
|
||||
"description": "Maak verbinding met de API van uw Advantage Air-tablet voor wandmontage.",
|
||||
|
||||
61
homeassistant/components/aemet/__init__.py
Normal file
61
homeassistant/components/aemet/__init__.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""The AEMET OpenData component."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aemet_opendata.interface import AEMET
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import COMPONENTS, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||
"""Set up the AEMET OpenData component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"""Set up AEMET OpenData as config entry."""
|
||||
name = config_entry.data[CONF_NAME]
|
||||
api_key = config_entry.data[CONF_API_KEY]
|
||||
latitude = config_entry.data[CONF_LATITUDE]
|
||||
longitude = config_entry.data[CONF_LONGITUDE]
|
||||
|
||||
aemet = AEMET(api_key)
|
||||
weather_coordinator = WeatherUpdateCoordinator(hass, aemet, latitude, longitude)
|
||||
|
||||
await weather_coordinator.async_refresh()
|
||||
|
||||
hass.data[DOMAIN][config_entry.entry_id] = {
|
||||
ENTRY_NAME: name,
|
||||
ENTRY_WEATHER_COORDINATOR: weather_coordinator,
|
||||
}
|
||||
|
||||
for component in COMPONENTS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(config_entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, component)
|
||||
for component in COMPONENTS
|
||||
]
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
57
homeassistant/components/aemet/abstract_aemet_sensor.py
Normal file
57
homeassistant/components/aemet/abstract_aemet_sensor.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""Abstraction form AEMET OpenData sensors."""
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTRIBUTION, SENSOR_DEVICE_CLASS, SENSOR_NAME, SENSOR_UNIT
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
|
||||
|
||||
class AbstractAemetSensor(CoordinatorEntity):
|
||||
"""Abstract class for an AEMET OpenData sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
sensor_configuration,
|
||||
coordinator: WeatherUpdateCoordinator,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._name = name
|
||||
self._unique_id = unique_id
|
||||
self._sensor_type = sensor_type
|
||||
self._sensor_name = sensor_configuration[SENSOR_NAME]
|
||||
self._unit_of_measurement = sensor_configuration.get(SENSOR_UNIT)
|
||||
self._device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return f"{self._name} {self._sensor_name}"
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique_id for this entity."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
"""Return the attribution."""
|
||||
return ATTRIBUTION
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device_class."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||
58
homeassistant/components/aemet/config_flow.py
Normal file
58
homeassistant/components/aemet/config_flow.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""Config flow for AEMET OpenData."""
|
||||
from aemet_opendata import AEMET
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import DEFAULT_NAME
|
||||
from .const import DOMAIN # pylint:disable=unused-import
|
||||
|
||||
|
||||
class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for AEMET OpenData."""
|
||||
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
latitude = user_input[CONF_LATITUDE]
|
||||
longitude = user_input[CONF_LONGITUDE]
|
||||
|
||||
await self.async_set_unique_id(f"{latitude}-{longitude}")
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
api_online = await _is_aemet_api_online(self.hass, user_input[CONF_API_KEY])
|
||||
if not api_online:
|
||||
errors["base"] = "invalid_api_key"
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_NAME], data=user_input
|
||||
)
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
|
||||
vol.Optional(
|
||||
CONF_LATITUDE, default=self.hass.config.latitude
|
||||
): cv.latitude,
|
||||
vol.Optional(
|
||||
CONF_LONGITUDE, default=self.hass.config.longitude
|
||||
): cv.longitude,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||
|
||||
|
||||
async def _is_aemet_api_online(hass, api_key):
|
||||
aemet = AEMET(api_key)
|
||||
return await hass.async_add_executor_job(
|
||||
aemet.get_conventional_observation_stations, False
|
||||
)
|
||||
326
homeassistant/components/aemet/const.py
Normal file
326
homeassistant/components/aemet/const.py
Normal file
@@ -0,0 +1,326 @@
|
||||
"""Constant values for the AEMET OpenData component."""
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
ATTR_CONDITION_CLOUDY,
|
||||
ATTR_CONDITION_FOG,
|
||||
ATTR_CONDITION_LIGHTNING,
|
||||
ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
ATTR_CONDITION_PARTLYCLOUDY,
|
||||
ATTR_CONDITION_POURING,
|
||||
ATTR_CONDITION_RAINY,
|
||||
ATTR_CONDITION_SNOWY,
|
||||
ATTR_CONDITION_SUNNY,
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_PRECIPITATION,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||
ATTR_FORECAST_TEMP,
|
||||
ATTR_FORECAST_TEMP_LOW,
|
||||
ATTR_FORECAST_TIME,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
ATTR_FORECAST_WIND_SPEED,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
DEGREE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
PERCENTAGE,
|
||||
PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
PRESSURE_HPA,
|
||||
SPEED_KILOMETERS_PER_HOUR,
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
|
||||
ATTRIBUTION = "Powered by AEMET OpenData"
|
||||
COMPONENTS = ["sensor", "weather"]
|
||||
DEFAULT_NAME = "AEMET"
|
||||
DOMAIN = "aemet"
|
||||
ENTRY_NAME = "name"
|
||||
ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
|
||||
UPDATE_LISTENER = "update_listener"
|
||||
SENSOR_NAME = "sensor_name"
|
||||
SENSOR_UNIT = "sensor_unit"
|
||||
SENSOR_DEVICE_CLASS = "sensor_device_class"
|
||||
|
||||
ATTR_API_CONDITION = "condition"
|
||||
ATTR_API_FORECAST_DAILY = "forecast-daily"
|
||||
ATTR_API_FORECAST_HOURLY = "forecast-hourly"
|
||||
ATTR_API_HUMIDITY = "humidity"
|
||||
ATTR_API_PRESSURE = "pressure"
|
||||
ATTR_API_RAIN = "rain"
|
||||
ATTR_API_RAIN_PROB = "rain-probability"
|
||||
ATTR_API_SNOW = "snow"
|
||||
ATTR_API_SNOW_PROB = "snow-probability"
|
||||
ATTR_API_STATION_ID = "station-id"
|
||||
ATTR_API_STATION_NAME = "station-name"
|
||||
ATTR_API_STATION_TIMESTAMP = "station-timestamp"
|
||||
ATTR_API_STORM_PROB = "storm-probability"
|
||||
ATTR_API_TEMPERATURE = "temperature"
|
||||
ATTR_API_TEMPERATURE_FEELING = "temperature-feeling"
|
||||
ATTR_API_TOWN_ID = "town-id"
|
||||
ATTR_API_TOWN_NAME = "town-name"
|
||||
ATTR_API_TOWN_TIMESTAMP = "town-timestamp"
|
||||
ATTR_API_WIND_BEARING = "wind-bearing"
|
||||
ATTR_API_WIND_MAX_SPEED = "wind-max-speed"
|
||||
ATTR_API_WIND_SPEED = "wind-speed"
|
||||
|
||||
CONDITIONS_MAP = {
|
||||
ATTR_CONDITION_CLEAR_NIGHT: {
|
||||
"11n", # Despejado (de noche)
|
||||
},
|
||||
ATTR_CONDITION_CLOUDY: {
|
||||
"14", # Nuboso
|
||||
"14n", # Nuboso (de noche)
|
||||
"15", # Muy nuboso
|
||||
"15n", # Muy nuboso (de noche)
|
||||
"16", # Cubierto
|
||||
"16n", # Cubierto (de noche)
|
||||
"17", # Nubes altas
|
||||
"17n", # Nubes altas (de noche)
|
||||
},
|
||||
ATTR_CONDITION_FOG: {
|
||||
"81", # Niebla
|
||||
"81n", # Niebla (de noche)
|
||||
"82", # Bruma - Neblina
|
||||
"82n", # Bruma - Neblina (de noche)
|
||||
},
|
||||
ATTR_CONDITION_LIGHTNING: {
|
||||
"51", # Intervalos nubosos con tormenta
|
||||
"51n", # Intervalos nubosos con tormenta (de noche)
|
||||
"52", # Nuboso con tormenta
|
||||
"52n", # Nuboso con tormenta (de noche)
|
||||
"53", # Muy nuboso con tormenta
|
||||
"53n", # Muy nuboso con tormenta (de noche)
|
||||
"54", # Cubierto con tormenta
|
||||
"54n", # Cubierto con tormenta (de noche)
|
||||
},
|
||||
ATTR_CONDITION_LIGHTNING_RAINY: {
|
||||
"61", # Intervalos nubosos con tormenta y lluvia escasa
|
||||
"61n", # Intervalos nubosos con tormenta y lluvia escasa (de noche)
|
||||
"62", # Nuboso con tormenta y lluvia escasa
|
||||
"62n", # Nuboso con tormenta y lluvia escasa (de noche)
|
||||
"63", # Muy nuboso con tormenta y lluvia escasa
|
||||
"63n", # Muy nuboso con tormenta y lluvia escasa (de noche)
|
||||
"64", # Cubierto con tormenta y lluvia escasa
|
||||
"64n", # Cubierto con tormenta y lluvia escasa (de noche)
|
||||
},
|
||||
ATTR_CONDITION_PARTLYCLOUDY: {
|
||||
"12", # Poco nuboso
|
||||
"12n", # Poco nuboso (de noche)
|
||||
"13", # Intervalos nubosos
|
||||
"13n", # Intervalos nubosos (de noche)
|
||||
},
|
||||
ATTR_CONDITION_POURING: {
|
||||
"27", # Chubascos
|
||||
"27n", # Chubascos (de noche)
|
||||
},
|
||||
ATTR_CONDITION_RAINY: {
|
||||
"23", # Intervalos nubosos con lluvia
|
||||
"23n", # Intervalos nubosos con lluvia (de noche)
|
||||
"24", # Nuboso con lluvia
|
||||
"24n", # Nuboso con lluvia (de noche)
|
||||
"25", # Muy nuboso con lluvia
|
||||
"25n", # Muy nuboso con lluvia (de noche)
|
||||
"26", # Cubierto con lluvia
|
||||
"26n", # Cubierto con lluvia (de noche)
|
||||
"43", # Intervalos nubosos con lluvia escasa
|
||||
"43n", # Intervalos nubosos con lluvia escasa (de noche)
|
||||
"44", # Nuboso con lluvia escasa
|
||||
"44n", # Nuboso con lluvia escasa (de noche)
|
||||
"45", # Muy nuboso con lluvia escasa
|
||||
"45n", # Muy nuboso con lluvia escasa (de noche)
|
||||
"46", # Cubierto con lluvia escasa
|
||||
"46n", # Cubierto con lluvia escasa (de noche)
|
||||
},
|
||||
ATTR_CONDITION_SNOWY: {
|
||||
"33", # Intervalos nubosos con nieve
|
||||
"33n", # Intervalos nubosos con nieve (de noche)
|
||||
"34", # Nuboso con nieve
|
||||
"34n", # Nuboso con nieve (de noche)
|
||||
"35", # Muy nuboso con nieve
|
||||
"35n", # Muy nuboso con nieve (de noche)
|
||||
"36", # Cubierto con nieve
|
||||
"36n", # Cubierto con nieve (de noche)
|
||||
"71", # Intervalos nubosos con nieve escasa
|
||||
"71n", # Intervalos nubosos con nieve escasa (de noche)
|
||||
"72", # Nuboso con nieve escasa
|
||||
"72n", # Nuboso con nieve escasa (de noche)
|
||||
"73", # Muy nuboso con nieve escasa
|
||||
"73n", # Muy nuboso con nieve escasa (de noche)
|
||||
"74", # Cubierto con nieve escasa
|
||||
"74n", # Cubierto con nieve escasa (de noche)
|
||||
},
|
||||
ATTR_CONDITION_SUNNY: {
|
||||
"11", # Despejado
|
||||
},
|
||||
}
|
||||
|
||||
FORECAST_MONITORED_CONDITIONS = [
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_PRECIPITATION,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||
ATTR_FORECAST_TEMP,
|
||||
ATTR_FORECAST_TEMP_LOW,
|
||||
ATTR_FORECAST_TIME,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
ATTR_FORECAST_WIND_SPEED,
|
||||
]
|
||||
MONITORED_CONDITIONS = [
|
||||
ATTR_API_CONDITION,
|
||||
ATTR_API_HUMIDITY,
|
||||
ATTR_API_PRESSURE,
|
||||
ATTR_API_RAIN,
|
||||
ATTR_API_RAIN_PROB,
|
||||
ATTR_API_SNOW,
|
||||
ATTR_API_SNOW_PROB,
|
||||
ATTR_API_STATION_ID,
|
||||
ATTR_API_STATION_NAME,
|
||||
ATTR_API_STATION_TIMESTAMP,
|
||||
ATTR_API_STORM_PROB,
|
||||
ATTR_API_TEMPERATURE,
|
||||
ATTR_API_TEMPERATURE_FEELING,
|
||||
ATTR_API_TOWN_ID,
|
||||
ATTR_API_TOWN_NAME,
|
||||
ATTR_API_TOWN_TIMESTAMP,
|
||||
ATTR_API_WIND_BEARING,
|
||||
ATTR_API_WIND_MAX_SPEED,
|
||||
ATTR_API_WIND_SPEED,
|
||||
]
|
||||
|
||||
FORECAST_MODE_DAILY = "daily"
|
||||
FORECAST_MODE_HOURLY = "hourly"
|
||||
FORECAST_MODES = [
|
||||
FORECAST_MODE_DAILY,
|
||||
FORECAST_MODE_HOURLY,
|
||||
]
|
||||
FORECAST_MODE_ATTR_API = {
|
||||
FORECAST_MODE_DAILY: ATTR_API_FORECAST_DAILY,
|
||||
FORECAST_MODE_HOURLY: ATTR_API_FORECAST_HOURLY,
|
||||
}
|
||||
|
||||
FORECAST_SENSOR_TYPES = {
|
||||
ATTR_FORECAST_CONDITION: {
|
||||
SENSOR_NAME: "Condition",
|
||||
},
|
||||
ATTR_FORECAST_PRECIPITATION: {
|
||||
SENSOR_NAME: "Precipitation",
|
||||
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: {
|
||||
SENSOR_NAME: "Precipitation probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_FORECAST_TEMP: {
|
||||
SENSOR_NAME: "Temperature",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_FORECAST_TEMP_LOW: {
|
||||
SENSOR_NAME: "Temperature Low",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_FORECAST_TIME: {
|
||||
SENSOR_NAME: "Time",
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
|
||||
},
|
||||
ATTR_FORECAST_WIND_BEARING: {
|
||||
SENSOR_NAME: "Wind bearing",
|
||||
SENSOR_UNIT: DEGREE,
|
||||
},
|
||||
ATTR_FORECAST_WIND_SPEED: {
|
||||
SENSOR_NAME: "Wind speed",
|
||||
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||
},
|
||||
}
|
||||
WEATHER_SENSOR_TYPES = {
|
||||
ATTR_API_CONDITION: {
|
||||
SENSOR_NAME: "Condition",
|
||||
},
|
||||
ATTR_API_HUMIDITY: {
|
||||
SENSOR_NAME: "Humidity",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
|
||||
},
|
||||
ATTR_API_PRESSURE: {
|
||||
SENSOR_NAME: "Pressure",
|
||||
SENSOR_UNIT: PRESSURE_HPA,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
|
||||
},
|
||||
ATTR_API_RAIN: {
|
||||
SENSOR_NAME: "Rain",
|
||||
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_API_RAIN_PROB: {
|
||||
SENSOR_NAME: "Rain probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_API_SNOW: {
|
||||
SENSOR_NAME: "Snow",
|
||||
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_API_SNOW_PROB: {
|
||||
SENSOR_NAME: "Snow probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_API_STATION_ID: {
|
||||
SENSOR_NAME: "Station ID",
|
||||
},
|
||||
ATTR_API_STATION_NAME: {
|
||||
SENSOR_NAME: "Station name",
|
||||
},
|
||||
ATTR_API_STATION_TIMESTAMP: {
|
||||
SENSOR_NAME: "Station timestamp",
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
|
||||
},
|
||||
ATTR_API_STORM_PROB: {
|
||||
SENSOR_NAME: "Storm probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_API_TEMPERATURE: {
|
||||
SENSOR_NAME: "Temperature",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_API_TEMPERATURE_FEELING: {
|
||||
SENSOR_NAME: "Temperature feeling",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_API_TOWN_ID: {
|
||||
SENSOR_NAME: "Town ID",
|
||||
},
|
||||
ATTR_API_TOWN_NAME: {
|
||||
SENSOR_NAME: "Town name",
|
||||
},
|
||||
ATTR_API_TOWN_TIMESTAMP: {
|
||||
SENSOR_NAME: "Town timestamp",
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
|
||||
},
|
||||
ATTR_API_WIND_BEARING: {
|
||||
SENSOR_NAME: "Wind bearing",
|
||||
SENSOR_UNIT: DEGREE,
|
||||
},
|
||||
ATTR_API_WIND_MAX_SPEED: {
|
||||
SENSOR_NAME: "Wind max speed",
|
||||
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_API_WIND_SPEED: {
|
||||
SENSOR_NAME: "Wind speed",
|
||||
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||
},
|
||||
}
|
||||
|
||||
WIND_BEARING_MAP = {
|
||||
"C": None,
|
||||
"N": 0.0,
|
||||
"NE": 45.0,
|
||||
"E": 90.0,
|
||||
"SE": 135.0,
|
||||
"S": 180.0,
|
||||
"SO": 225.0,
|
||||
"O": 270.0,
|
||||
"NO": 315.0,
|
||||
}
|
||||
8
homeassistant/components/aemet/manifest.json
Normal file
8
homeassistant/components/aemet/manifest.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"domain": "aemet",
|
||||
"name": "AEMET OpenData",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||
"requirements": ["AEMET-OpenData==0.1.8"],
|
||||
"codeowners": ["@noltari"]
|
||||
}
|
||||
114
homeassistant/components/aemet/sensor.py
Normal file
114
homeassistant/components/aemet/sensor.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""Support for the AEMET OpenData service."""
|
||||
from .abstract_aemet_sensor import AbstractAemetSensor
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
ENTRY_NAME,
|
||||
ENTRY_WEATHER_COORDINATOR,
|
||||
FORECAST_MODE_ATTR_API,
|
||||
FORECAST_MODE_DAILY,
|
||||
FORECAST_MODES,
|
||||
FORECAST_MONITORED_CONDITIONS,
|
||||
FORECAST_SENSOR_TYPES,
|
||||
MONITORED_CONDITIONS,
|
||||
WEATHER_SENSOR_TYPES,
|
||||
)
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up AEMET OpenData sensor entities based on a config entry."""
|
||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
name = domain_data[ENTRY_NAME]
|
||||
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
||||
|
||||
weather_sensor_types = WEATHER_SENSOR_TYPES
|
||||
forecast_sensor_types = FORECAST_SENSOR_TYPES
|
||||
|
||||
entities = []
|
||||
for sensor_type in MONITORED_CONDITIONS:
|
||||
unique_id = f"{config_entry.unique_id}-{sensor_type}"
|
||||
entities.append(
|
||||
AemetSensor(
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
weather_sensor_types[sensor_type],
|
||||
weather_coordinator,
|
||||
)
|
||||
)
|
||||
|
||||
for mode in FORECAST_MODES:
|
||||
name = f"{domain_data[ENTRY_NAME]} {mode}"
|
||||
|
||||
for sensor_type in FORECAST_MONITORED_CONDITIONS:
|
||||
unique_id = f"{config_entry.unique_id}-forecast-{mode}-{sensor_type}"
|
||||
entities.append(
|
||||
AemetForecastSensor(
|
||||
f"{name} Forecast",
|
||||
unique_id,
|
||||
sensor_type,
|
||||
forecast_sensor_types[sensor_type],
|
||||
weather_coordinator,
|
||||
mode,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AemetSensor(AbstractAemetSensor):
|
||||
"""Implementation of an AEMET OpenData sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
sensor_configuration,
|
||||
weather_coordinator: WeatherUpdateCoordinator,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(
|
||||
name, unique_id, sensor_type, sensor_configuration, weather_coordinator
|
||||
)
|
||||
self._weather_coordinator = weather_coordinator
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._weather_coordinator.data.get(self._sensor_type)
|
||||
|
||||
|
||||
class AemetForecastSensor(AbstractAemetSensor):
|
||||
"""Implementation of an AEMET OpenData forecast sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
sensor_configuration,
|
||||
weather_coordinator: WeatherUpdateCoordinator,
|
||||
forecast_mode,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(
|
||||
name, unique_id, sensor_type, sensor_configuration, weather_coordinator
|
||||
)
|
||||
self._weather_coordinator = weather_coordinator
|
||||
self._forecast_mode = forecast_mode
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
return self._forecast_mode == FORECAST_MODE_DAILY
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
forecasts = self._weather_coordinator.data.get(
|
||||
FORECAST_MODE_ATTR_API[self._forecast_mode]
|
||||
)
|
||||
if forecasts:
|
||||
return forecasts[0].get(self._sensor_type)
|
||||
return None
|
||||
22
homeassistant/components/aemet/strings.json
Normal file
22
homeassistant/components/aemet/strings.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"latitude": "[%key:common::config_flow::data::latitude%]",
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]",
|
||||
"name": "Name of the integration"
|
||||
},
|
||||
"description": "Set up AEMET OpenData integration. To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
homeassistant/components/aemet/translations/ca.json
Normal file
22
homeassistant/components/aemet/translations/ca.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Clau API inv\u00e0lida"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Clau API",
|
||||
"latitude": "Latitud",
|
||||
"longitude": "Longitud",
|
||||
"name": "Nom de la integraci\u00f3"
|
||||
},
|
||||
"description": "Configura la integraci\u00f3 d'AEMET OpenData. Per generar la clau API, v\u00e9s a https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
homeassistant/components/aemet/translations/cs.json
Normal file
20
homeassistant/components/aemet/translations/cs.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Kl\u00ed\u010d API",
|
||||
"latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka",
|
||||
"longitude": "Zem\u011bpisn\u00e1 d\u00e9lka",
|
||||
"name": "N\u00e1zev integrace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
homeassistant/components/aemet/translations/en.json
Normal file
22
homeassistant/components/aemet/translations/en.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Location is already configured"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Invalid API key"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"name": "Name of the integration"
|
||||
},
|
||||
"description": "Set up AEMET OpenData integration. To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
homeassistant/components/aemet/translations/es.json
Normal file
22
homeassistant/components/aemet/translations/es.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Clave API no v\u00e1lida"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Clave API",
|
||||
"latitude": "Latitud",
|
||||
"longitude": "Longitud",
|
||||
"name": "Nombre de la integraci\u00f3n"
|
||||
},
|
||||
"description": "Configurar la integraci\u00f3n de AEMET OpenData. Para generar la clave API, ve a https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
homeassistant/components/aemet/translations/et.json
Normal file
22
homeassistant/components/aemet/translations/et.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Asukoht on juba m\u00e4\u00e4ratud"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Vale API v\u00f5ti"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API v\u00f5ti",
|
||||
"latitude": "Laiuskraad",
|
||||
"longitude": "Pikkuskraad",
|
||||
"name": "Sidumise nimi"
|
||||
},
|
||||
"description": "Seadista AEMET OpenData sidumine. API v\u00f5tme loomiseks mine aadressile https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
homeassistant/components/aemet/translations/fr.json
Normal file
22
homeassistant/components/aemet/translations/fr.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Cl\u00e9 API invalide"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Cl\u00e9 d'API",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"name": "Nom de l'int\u00e9gration"
|
||||
},
|
||||
"description": "Configurez l'int\u00e9gration AEMET OpenData. Pour g\u00e9n\u00e9rer la cl\u00e9 API, acc\u00e9dez \u00e0 https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
homeassistant/components/aemet/translations/it.json
Normal file
22
homeassistant/components/aemet/translations/it.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "La posizione \u00e8 gi\u00e0 configurata"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Chiave API non valida"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Chiave API",
|
||||
"latitude": "Latitudine",
|
||||
"longitude": "Logitudine",
|
||||
"name": "Nome dell'integrazione"
|
||||
},
|
||||
"description": "Imposta l'integrazione di AEMET OpenData. Per generare la chiave API, vai su https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
homeassistant/components/aemet/translations/ko.json
Normal file
21
homeassistant/components/aemet/translations/ko.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API \ud0a4",
|
||||
"latitude": "\uc704\ub3c4",
|
||||
"longitude": "\uacbd\ub3c4",
|
||||
"name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc774\ub984"
|
||||
},
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
homeassistant/components/aemet/translations/nl.json
Normal file
21
homeassistant/components/aemet/translations/nl.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Locatie is al geconfigureerd."
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Ongeldige API-sleutel"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API-sleutel",
|
||||
"latitude": "Breedtegraad",
|
||||
"longitude": "Lengtegraad",
|
||||
"name": "Naam van de integratie"
|
||||
},
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
homeassistant/components/aemet/translations/no.json
Normal file
22
homeassistant/components/aemet/translations/no.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Plasseringen er allerede konfigurert"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Ugyldig API-n\u00f8kkel"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API-n\u00f8kkel",
|
||||
"latitude": "Breddegrad",
|
||||
"longitude": "Lengdegrad",
|
||||
"name": "Navnet p\u00e5 integrasjonen"
|
||||
},
|
||||
"description": "Sett opp AEMET OpenData-integrasjon. For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
homeassistant/components/aemet/translations/pl.json
Normal file
22
homeassistant/components/aemet/translations/pl.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Lokalizacja jest ju\u017c skonfigurowana"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Nieprawid\u0142owy klucz API"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Klucz API",
|
||||
"latitude": "Szeroko\u015b\u0107 geograficzna",
|
||||
"longitude": "D\u0142ugo\u015b\u0107 geograficzna",
|
||||
"name": "Nazwa integracji"
|
||||
},
|
||||
"description": "Skonfiguruj integracj\u0119 AEMET OpenData. Aby wygenerowa\u0107 klucz API, przejd\u017a do https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
homeassistant/components/aemet/translations/ru.json
Normal file
22
homeassistant/components/aemet/translations/ru.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "\u041a\u043b\u044e\u0447 API",
|
||||
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
|
||||
"longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430",
|
||||
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435"
|
||||
},
|
||||
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 AEMET OpenData. \u0427\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://opendata.aemet.es/centrodedescargas/altaUsuario.",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
homeassistant/components/aemet/translations/zh-Hant.json
Normal file
22
homeassistant/components/aemet/translations/zh-Hant.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "API \u5bc6\u9470\u7121\u6548"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API \u5bc6\u9470",
|
||||
"latitude": "\u7def\u5ea6",
|
||||
"longitude": "\u7d93\u5ea6",
|
||||
"name": "\u6574\u5408\u540d\u7a31"
|
||||
},
|
||||
"description": "\u6b32\u8a2d\u5b9a AEMET OpenData \u6574\u5408\u3002\u8acb\u81f3 https://opendata.aemet.es/centrodedescargas/altaUsuario \u7522\u751f API \u5bc6\u9470",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
113
homeassistant/components/aemet/weather.py
Normal file
113
homeassistant/components/aemet/weather.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""Support for the AEMET OpenData service."""
|
||||
from homeassistant.components.weather import WeatherEntity
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
ATTR_API_CONDITION,
|
||||
ATTR_API_HUMIDITY,
|
||||
ATTR_API_PRESSURE,
|
||||
ATTR_API_TEMPERATURE,
|
||||
ATTR_API_WIND_BEARING,
|
||||
ATTR_API_WIND_SPEED,
|
||||
ATTRIBUTION,
|
||||
DOMAIN,
|
||||
ENTRY_NAME,
|
||||
ENTRY_WEATHER_COORDINATOR,
|
||||
FORECAST_MODE_ATTR_API,
|
||||
FORECAST_MODE_DAILY,
|
||||
FORECAST_MODES,
|
||||
)
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up AEMET OpenData weather entity based on a config entry."""
|
||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
||||
|
||||
entities = []
|
||||
for mode in FORECAST_MODES:
|
||||
name = f"{domain_data[ENTRY_NAME]} {mode}"
|
||||
unique_id = f"{config_entry.unique_id} {mode}"
|
||||
entities.append(AemetWeather(name, unique_id, weather_coordinator, mode))
|
||||
|
||||
if entities:
|
||||
async_add_entities(entities, False)
|
||||
|
||||
|
||||
class AemetWeather(CoordinatorEntity, WeatherEntity):
|
||||
"""Implementation of an AEMET OpenData sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
coordinator: WeatherUpdateCoordinator,
|
||||
forecast_mode,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._name = name
|
||||
self._unique_id = unique_id
|
||||
self._forecast_mode = forecast_mode
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
"""Return the attribution."""
|
||||
return ATTRIBUTION
|
||||
|
||||
@property
|
||||
def condition(self):
|
||||
"""Return the current condition."""
|
||||
return self.coordinator.data[ATTR_API_CONDITION]
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
return self._forecast_mode == FORECAST_MODE_DAILY
|
||||
|
||||
@property
|
||||
def forecast(self):
|
||||
"""Return the forecast array."""
|
||||
return self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]]
|
||||
|
||||
@property
|
||||
def humidity(self):
|
||||
"""Return the humidity."""
|
||||
return self.coordinator.data[ATTR_API_HUMIDITY]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def pressure(self):
|
||||
"""Return the pressure."""
|
||||
return self.coordinator.data[ATTR_API_PRESSURE]
|
||||
|
||||
@property
|
||||
def temperature(self):
|
||||
"""Return the temperature."""
|
||||
return self.coordinator.data[ATTR_API_TEMPERATURE]
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique_id for this entity."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def wind_bearing(self):
|
||||
"""Return the temperature."""
|
||||
return self.coordinator.data[ATTR_API_WIND_BEARING]
|
||||
|
||||
@property
|
||||
def wind_speed(self):
|
||||
"""Return the temperature."""
|
||||
return self.coordinator.data[ATTR_API_WIND_SPEED]
|
||||
637
homeassistant/components/aemet/weather_update_coordinator.py
Normal file
637
homeassistant/components/aemet/weather_update_coordinator.py
Normal file
@@ -0,0 +1,637 @@
|
||||
"""Weather data coordinator for the AEMET OpenData service."""
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aemet_opendata.const import (
|
||||
AEMET_ATTR_DATE,
|
||||
AEMET_ATTR_DAY,
|
||||
AEMET_ATTR_DIRECTION,
|
||||
AEMET_ATTR_ELABORATED,
|
||||
AEMET_ATTR_FORECAST,
|
||||
AEMET_ATTR_HUMIDITY,
|
||||
AEMET_ATTR_ID,
|
||||
AEMET_ATTR_IDEMA,
|
||||
AEMET_ATTR_MAX,
|
||||
AEMET_ATTR_MIN,
|
||||
AEMET_ATTR_NAME,
|
||||
AEMET_ATTR_PRECIPITATION,
|
||||
AEMET_ATTR_PRECIPITATION_PROBABILITY,
|
||||
AEMET_ATTR_SKY_STATE,
|
||||
AEMET_ATTR_SNOW,
|
||||
AEMET_ATTR_SNOW_PROBABILITY,
|
||||
AEMET_ATTR_SPEED,
|
||||
AEMET_ATTR_STATION_DATE,
|
||||
AEMET_ATTR_STATION_HUMIDITY,
|
||||
AEMET_ATTR_STATION_LOCATION,
|
||||
AEMET_ATTR_STATION_PRESSURE_SEA,
|
||||
AEMET_ATTR_STATION_TEMPERATURE,
|
||||
AEMET_ATTR_STORM_PROBABILITY,
|
||||
AEMET_ATTR_TEMPERATURE,
|
||||
AEMET_ATTR_TEMPERATURE_FEELING,
|
||||
AEMET_ATTR_WIND,
|
||||
AEMET_ATTR_WIND_GUST,
|
||||
ATTR_DATA,
|
||||
)
|
||||
from aemet_opendata.helpers import (
|
||||
get_forecast_day_value,
|
||||
get_forecast_hour_value,
|
||||
get_forecast_interval_value,
|
||||
)
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_PRECIPITATION,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||
ATTR_FORECAST_TEMP,
|
||||
ATTR_FORECAST_TEMP_LOW,
|
||||
ATTR_FORECAST_TIME,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
ATTR_FORECAST_WIND_SPEED,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
ATTR_API_CONDITION,
|
||||
ATTR_API_FORECAST_DAILY,
|
||||
ATTR_API_FORECAST_HOURLY,
|
||||
ATTR_API_HUMIDITY,
|
||||
ATTR_API_PRESSURE,
|
||||
ATTR_API_RAIN,
|
||||
ATTR_API_RAIN_PROB,
|
||||
ATTR_API_SNOW,
|
||||
ATTR_API_SNOW_PROB,
|
||||
ATTR_API_STATION_ID,
|
||||
ATTR_API_STATION_NAME,
|
||||
ATTR_API_STATION_TIMESTAMP,
|
||||
ATTR_API_STORM_PROB,
|
||||
ATTR_API_TEMPERATURE,
|
||||
ATTR_API_TEMPERATURE_FEELING,
|
||||
ATTR_API_TOWN_ID,
|
||||
ATTR_API_TOWN_NAME,
|
||||
ATTR_API_TOWN_TIMESTAMP,
|
||||
ATTR_API_WIND_BEARING,
|
||||
ATTR_API_WIND_MAX_SPEED,
|
||||
ATTR_API_WIND_SPEED,
|
||||
CONDITIONS_MAP,
|
||||
DOMAIN,
|
||||
WIND_BEARING_MAP,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
|
||||
def format_condition(condition: str) -> str:
|
||||
"""Return condition from dict CONDITIONS_MAP."""
|
||||
for key, value in CONDITIONS_MAP.items():
|
||||
if condition in value:
|
||||
return key
|
||||
_LOGGER.error('condition "%s" not found in CONDITIONS_MAP', condition)
|
||||
return condition
|
||||
|
||||
|
||||
def format_float(value) -> float:
|
||||
"""Try converting string to float."""
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def format_int(value) -> int:
|
||||
"""Try converting string to int."""
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
class TownNotFound(UpdateFailed):
|
||||
"""Raised when town is not found."""
|
||||
|
||||
|
||||
class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Weather data update coordinator."""
|
||||
|
||||
def __init__(self, hass, aemet, latitude, longitude):
|
||||
"""Initialize coordinator."""
|
||||
super().__init__(
|
||||
hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL
|
||||
)
|
||||
|
||||
self._aemet = aemet
|
||||
self._station = None
|
||||
self._town = None
|
||||
self._latitude = latitude
|
||||
self._longitude = longitude
|
||||
self._data = {
|
||||
"daily": None,
|
||||
"hourly": None,
|
||||
"station": None,
|
||||
}
|
||||
|
||||
async def _async_update_data(self):
|
||||
data = {}
|
||||
with async_timeout.timeout(120):
|
||||
weather_response = await self._get_aemet_weather()
|
||||
data = self._convert_weather_response(weather_response)
|
||||
return data
|
||||
|
||||
async def _get_aemet_weather(self):
|
||||
"""Poll weather data from AEMET OpenData."""
|
||||
weather = await self.hass.async_add_executor_job(self._get_weather_and_forecast)
|
||||
return weather
|
||||
|
||||
def _get_weather_station(self):
|
||||
if not self._station:
|
||||
self._station = (
|
||||
self._aemet.get_conventional_observation_station_by_coordinates(
|
||||
self._latitude, self._longitude
|
||||
)
|
||||
)
|
||||
if self._station:
|
||||
_LOGGER.debug(
|
||||
"station found for coordinates [%s, %s]: %s",
|
||||
self._latitude,
|
||||
self._longitude,
|
||||
self._station,
|
||||
)
|
||||
if not self._station:
|
||||
_LOGGER.debug(
|
||||
"station not found for coordinates [%s, %s]",
|
||||
self._latitude,
|
||||
self._longitude,
|
||||
)
|
||||
return self._station
|
||||
|
||||
def _get_weather_town(self):
|
||||
if not self._town:
|
||||
self._town = self._aemet.get_town_by_coordinates(
|
||||
self._latitude, self._longitude
|
||||
)
|
||||
if self._town:
|
||||
_LOGGER.debug(
|
||||
"town found for coordinates [%s, %s]: %s",
|
||||
self._latitude,
|
||||
self._longitude,
|
||||
self._town,
|
||||
)
|
||||
if not self._town:
|
||||
_LOGGER.error(
|
||||
"town not found for coordinates [%s, %s]",
|
||||
self._latitude,
|
||||
self._longitude,
|
||||
)
|
||||
raise TownNotFound
|
||||
return self._town
|
||||
|
||||
def _get_weather_and_forecast(self):
|
||||
"""Get weather and forecast data from AEMET OpenData."""
|
||||
|
||||
self._get_weather_town()
|
||||
|
||||
daily = self._aemet.get_specific_forecast_town_daily(self._town[AEMET_ATTR_ID])
|
||||
if not daily:
|
||||
_LOGGER.error(
|
||||
'error fetching daily data for town "%s"', self._town[AEMET_ATTR_ID]
|
||||
)
|
||||
|
||||
hourly = self._aemet.get_specific_forecast_town_hourly(
|
||||
self._town[AEMET_ATTR_ID]
|
||||
)
|
||||
if not hourly:
|
||||
_LOGGER.error(
|
||||
'error fetching hourly data for town "%s"', self._town[AEMET_ATTR_ID]
|
||||
)
|
||||
|
||||
station = None
|
||||
if self._get_weather_station():
|
||||
station = self._aemet.get_conventional_observation_station_data(
|
||||
self._station[AEMET_ATTR_IDEMA]
|
||||
)
|
||||
if not station:
|
||||
_LOGGER.error(
|
||||
'error fetching data for station "%s"',
|
||||
self._station[AEMET_ATTR_IDEMA],
|
||||
)
|
||||
|
||||
if daily:
|
||||
self._data["daily"] = daily
|
||||
if hourly:
|
||||
self._data["hourly"] = hourly
|
||||
if station:
|
||||
self._data["station"] = station
|
||||
|
||||
return AemetWeather(
|
||||
self._data["daily"],
|
||||
self._data["hourly"],
|
||||
self._data["station"],
|
||||
)
|
||||
|
||||
def _convert_weather_response(self, weather_response):
|
||||
"""Format the weather response correctly."""
|
||||
if not weather_response or not weather_response.hourly:
|
||||
return None
|
||||
|
||||
elaborated = dt_util.parse_datetime(
|
||||
weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_ELABORATED]
|
||||
)
|
||||
now = dt_util.now()
|
||||
hour = now.hour
|
||||
|
||||
# Get current day
|
||||
day = None
|
||||
for cur_day in weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_FORECAST][
|
||||
AEMET_ATTR_DAY
|
||||
]:
|
||||
cur_day_date = dt_util.parse_datetime(cur_day[AEMET_ATTR_DATE])
|
||||
if now.date() == cur_day_date.date():
|
||||
day = cur_day
|
||||
break
|
||||
|
||||
# Get station data
|
||||
station_data = None
|
||||
if weather_response.station:
|
||||
station_data = weather_response.station[ATTR_DATA][-1]
|
||||
|
||||
condition = None
|
||||
humidity = None
|
||||
pressure = None
|
||||
rain = None
|
||||
rain_prob = None
|
||||
snow = None
|
||||
snow_prob = None
|
||||
station_id = None
|
||||
station_name = None
|
||||
station_timestamp = None
|
||||
storm_prob = None
|
||||
temperature = None
|
||||
temperature_feeling = None
|
||||
town_id = None
|
||||
town_name = None
|
||||
town_timestamp = dt_util.as_utc(elaborated)
|
||||
wind_bearing = None
|
||||
wind_max_speed = None
|
||||
wind_speed = None
|
||||
|
||||
# Get weather values
|
||||
if day:
|
||||
condition = self._get_condition(day, hour)
|
||||
humidity = self._get_humidity(day, hour)
|
||||
rain = self._get_rain(day, hour)
|
||||
rain_prob = self._get_rain_prob(day, hour)
|
||||
snow = self._get_snow(day, hour)
|
||||
snow_prob = self._get_snow_prob(day, hour)
|
||||
station_id = self._get_station_id()
|
||||
station_name = self._get_station_name()
|
||||
storm_prob = self._get_storm_prob(day, hour)
|
||||
temperature = self._get_temperature(day, hour)
|
||||
temperature_feeling = self._get_temperature_feeling(day, hour)
|
||||
town_id = self._get_town_id()
|
||||
town_name = self._get_town_name()
|
||||
wind_bearing = self._get_wind_bearing(day, hour)
|
||||
wind_max_speed = self._get_wind_max_speed(day, hour)
|
||||
wind_speed = self._get_wind_speed(day, hour)
|
||||
|
||||
# Overwrite weather values with closest station data (if present)
|
||||
if station_data:
|
||||
if AEMET_ATTR_STATION_DATE in station_data:
|
||||
station_dt = dt_util.parse_datetime(
|
||||
station_data[AEMET_ATTR_STATION_DATE] + "Z"
|
||||
)
|
||||
station_timestamp = dt_util.as_utc(station_dt).isoformat()
|
||||
if AEMET_ATTR_STATION_HUMIDITY in station_data:
|
||||
humidity = format_float(station_data[AEMET_ATTR_STATION_HUMIDITY])
|
||||
if AEMET_ATTR_STATION_PRESSURE_SEA in station_data:
|
||||
pressure = format_float(station_data[AEMET_ATTR_STATION_PRESSURE_SEA])
|
||||
if AEMET_ATTR_STATION_TEMPERATURE in station_data:
|
||||
temperature = format_float(station_data[AEMET_ATTR_STATION_TEMPERATURE])
|
||||
|
||||
# Get forecast from weather data
|
||||
forecast_daily = self._get_daily_forecast_from_weather_response(
|
||||
weather_response, now
|
||||
)
|
||||
forecast_hourly = self._get_hourly_forecast_from_weather_response(
|
||||
weather_response, now
|
||||
)
|
||||
|
||||
return {
|
||||
ATTR_API_CONDITION: condition,
|
||||
ATTR_API_FORECAST_DAILY: forecast_daily,
|
||||
ATTR_API_FORECAST_HOURLY: forecast_hourly,
|
||||
ATTR_API_HUMIDITY: humidity,
|
||||
ATTR_API_TEMPERATURE: temperature,
|
||||
ATTR_API_TEMPERATURE_FEELING: temperature_feeling,
|
||||
ATTR_API_PRESSURE: pressure,
|
||||
ATTR_API_RAIN: rain,
|
||||
ATTR_API_RAIN_PROB: rain_prob,
|
||||
ATTR_API_SNOW: snow,
|
||||
ATTR_API_SNOW_PROB: snow_prob,
|
||||
ATTR_API_STATION_ID: station_id,
|
||||
ATTR_API_STATION_NAME: station_name,
|
||||
ATTR_API_STATION_TIMESTAMP: station_timestamp,
|
||||
ATTR_API_STORM_PROB: storm_prob,
|
||||
ATTR_API_TOWN_ID: town_id,
|
||||
ATTR_API_TOWN_NAME: town_name,
|
||||
ATTR_API_TOWN_TIMESTAMP: town_timestamp,
|
||||
ATTR_API_WIND_BEARING: wind_bearing,
|
||||
ATTR_API_WIND_MAX_SPEED: wind_max_speed,
|
||||
ATTR_API_WIND_SPEED: wind_speed,
|
||||
}
|
||||
|
||||
def _get_daily_forecast_from_weather_response(self, weather_response, now):
|
||||
if weather_response.daily:
|
||||
parse = False
|
||||
forecast = []
|
||||
for day in weather_response.daily[ATTR_DATA][0][AEMET_ATTR_FORECAST][
|
||||
AEMET_ATTR_DAY
|
||||
]:
|
||||
day_date = dt_util.parse_datetime(day[AEMET_ATTR_DATE])
|
||||
if now.date() == day_date.date():
|
||||
parse = True
|
||||
if parse:
|
||||
cur_forecast = self._convert_forecast_day(day_date, day)
|
||||
if cur_forecast:
|
||||
forecast.append(cur_forecast)
|
||||
return forecast
|
||||
return None
|
||||
|
||||
def _get_hourly_forecast_from_weather_response(self, weather_response, now):
|
||||
if weather_response.hourly:
|
||||
parse = False
|
||||
hour = now.hour
|
||||
forecast = []
|
||||
for day in weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_FORECAST][
|
||||
AEMET_ATTR_DAY
|
||||
]:
|
||||
day_date = dt_util.parse_datetime(day[AEMET_ATTR_DATE])
|
||||
hour_start = 0
|
||||
if now.date() == day_date.date():
|
||||
parse = True
|
||||
hour_start = now.hour
|
||||
if parse:
|
||||
for hour in range(hour_start, 24):
|
||||
cur_forecast = self._convert_forecast_hour(day_date, day, hour)
|
||||
if cur_forecast:
|
||||
forecast.append(cur_forecast)
|
||||
return forecast
|
||||
return None
|
||||
|
||||
def _convert_forecast_day(self, date, day):
|
||||
condition = self._get_condition_day(day)
|
||||
if not condition:
|
||||
return None
|
||||
|
||||
return {
|
||||
ATTR_FORECAST_CONDITION: condition,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day(
|
||||
day
|
||||
),
|
||||
ATTR_FORECAST_TEMP: self._get_temperature_day(day),
|
||||
ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day),
|
||||
ATTR_FORECAST_TIME: dt_util.as_utc(date),
|
||||
ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day),
|
||||
ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day),
|
||||
}
|
||||
|
||||
def _convert_forecast_hour(self, date, day, hour):
|
||||
condition = self._get_condition(day, hour)
|
||||
if not condition:
|
||||
return None
|
||||
|
||||
forecast_dt = date.replace(hour=hour, minute=0, second=0)
|
||||
|
||||
return {
|
||||
ATTR_FORECAST_CONDITION: condition,
|
||||
ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour),
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob(
|
||||
day, hour
|
||||
),
|
||||
ATTR_FORECAST_TEMP: self._get_temperature(day, hour),
|
||||
ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt),
|
||||
ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour),
|
||||
ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour),
|
||||
}
|
||||
|
||||
def _calc_precipitation(self, day, hour):
|
||||
"""Calculate the precipitation."""
|
||||
rain_value = self._get_rain(day, hour)
|
||||
if not rain_value:
|
||||
rain_value = 0
|
||||
|
||||
snow_value = self._get_snow(day, hour)
|
||||
if not snow_value:
|
||||
snow_value = 0
|
||||
|
||||
if round(rain_value + snow_value, 1) == 0:
|
||||
return None
|
||||
return round(rain_value + snow_value, 1)
|
||||
|
||||
def _calc_precipitation_prob(self, day, hour):
|
||||
"""Calculate the precipitation probability (hour)."""
|
||||
rain_value = self._get_rain_prob(day, hour)
|
||||
if not rain_value:
|
||||
rain_value = 0
|
||||
|
||||
snow_value = self._get_snow_prob(day, hour)
|
||||
if not snow_value:
|
||||
snow_value = 0
|
||||
|
||||
if rain_value == 0 and snow_value == 0:
|
||||
return None
|
||||
return max(rain_value, snow_value)
|
||||
|
||||
@staticmethod
|
||||
def _get_condition(day_data, hour):
|
||||
"""Get weather condition (hour) from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_SKY_STATE], hour)
|
||||
if val:
|
||||
return format_condition(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_condition_day(day_data):
|
||||
"""Get weather condition (day) from weather data."""
|
||||
val = get_forecast_day_value(day_data[AEMET_ATTR_SKY_STATE])
|
||||
if val:
|
||||
return format_condition(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_humidity(day_data, hour):
|
||||
"""Get humidity from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_HUMIDITY], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_precipitation_prob_day(day_data):
|
||||
"""Get humidity from weather data."""
|
||||
val = get_forecast_day_value(day_data[AEMET_ATTR_PRECIPITATION_PROBABILITY])
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_rain(day_data, hour):
|
||||
"""Get rain from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_PRECIPITATION], hour)
|
||||
if val:
|
||||
return format_float(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_rain_prob(day_data, hour):
|
||||
"""Get rain probability from weather data."""
|
||||
val = get_forecast_interval_value(
|
||||
day_data[AEMET_ATTR_PRECIPITATION_PROBABILITY], hour
|
||||
)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_snow(day_data, hour):
|
||||
"""Get snow from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_SNOW], hour)
|
||||
if val:
|
||||
return format_float(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_snow_prob(day_data, hour):
|
||||
"""Get snow probability from weather data."""
|
||||
val = get_forecast_interval_value(day_data[AEMET_ATTR_SNOW_PROBABILITY], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
def _get_station_id(self):
|
||||
"""Get station ID from weather data."""
|
||||
if self._station:
|
||||
return self._station[AEMET_ATTR_IDEMA]
|
||||
return None
|
||||
|
||||
def _get_station_name(self):
|
||||
"""Get station name from weather data."""
|
||||
if self._station:
|
||||
return self._station[AEMET_ATTR_STATION_LOCATION]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_storm_prob(day_data, hour):
|
||||
"""Get storm probability from weather data."""
|
||||
val = get_forecast_interval_value(day_data[AEMET_ATTR_STORM_PROBABILITY], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_temperature(day_data, hour):
|
||||
"""Get temperature (hour) from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_temperature_day(day_data):
|
||||
"""Get temperature (day) from weather data."""
|
||||
val = get_forecast_day_value(
|
||||
day_data[AEMET_ATTR_TEMPERATURE], key=AEMET_ATTR_MAX
|
||||
)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_temperature_low_day(day_data):
|
||||
"""Get temperature (day) from weather data."""
|
||||
val = get_forecast_day_value(
|
||||
day_data[AEMET_ATTR_TEMPERATURE], key=AEMET_ATTR_MIN
|
||||
)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_temperature_feeling(day_data, hour):
|
||||
"""Get temperature from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE_FEELING], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
def _get_town_id(self):
|
||||
"""Get town ID from weather data."""
|
||||
if self._town:
|
||||
return self._town[AEMET_ATTR_ID]
|
||||
return None
|
||||
|
||||
def _get_town_name(self):
|
||||
"""Get town name from weather data."""
|
||||
if self._town:
|
||||
return self._town[AEMET_ATTR_NAME]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_wind_bearing(day_data, hour):
|
||||
"""Get wind bearing (hour) from weather data."""
|
||||
val = get_forecast_hour_value(
|
||||
day_data[AEMET_ATTR_WIND_GUST], hour, key=AEMET_ATTR_DIRECTION
|
||||
)[0]
|
||||
if val in WIND_BEARING_MAP:
|
||||
return WIND_BEARING_MAP[val]
|
||||
_LOGGER.error("%s not found in Wind Bearing map", val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_wind_bearing_day(day_data):
|
||||
"""Get wind bearing (day) from weather data."""
|
||||
val = get_forecast_day_value(
|
||||
day_data[AEMET_ATTR_WIND], key=AEMET_ATTR_DIRECTION
|
||||
)
|
||||
if val in WIND_BEARING_MAP:
|
||||
return WIND_BEARING_MAP[val]
|
||||
_LOGGER.error("%s not found in Wind Bearing map", val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_wind_max_speed(day_data, hour):
|
||||
"""Get wind max speed from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_WIND_GUST], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_wind_speed(day_data, hour):
|
||||
"""Get wind speed (hour) from weather data."""
|
||||
val = get_forecast_hour_value(
|
||||
day_data[AEMET_ATTR_WIND_GUST], hour, key=AEMET_ATTR_SPEED
|
||||
)[0]
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_wind_speed_day(day_data):
|
||||
"""Get wind speed (day) from weather data."""
|
||||
val = get_forecast_day_value(day_data[AEMET_ATTR_WIND], key=AEMET_ATTR_SPEED)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AemetWeather:
|
||||
"""Class to harmonize weather data model."""
|
||||
|
||||
daily: dict = field(default_factory=dict)
|
||||
hourly: dict = field(default_factory=dict)
|
||||
station: dict = field(default_factory=dict)
|
||||
@@ -1,6 +1,4 @@
|
||||
"""Config flow to configure Agent devices."""
|
||||
import logging
|
||||
|
||||
from agent import AgentConnectionError, AgentError
|
||||
from agent.a import Agent
|
||||
import voluptuous as vol
|
||||
@@ -13,7 +11,6 @@ from .const import DOMAIN, SERVER_URL # pylint:disable=unused-import
|
||||
from .helpers import generate_url
|
||||
|
||||
DEFAULT_PORT = 8090
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"error": {
|
||||
"already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4."
|
||||
"already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"already_configured": "Apparaat is al geconfigureerd"
|
||||
},
|
||||
"error": {
|
||||
"already_in_progress": "De configuratiestroom is al aan de gang",
|
||||
"cannot_connect": "Kan geen verbinding maken"
|
||||
},
|
||||
"step": {
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
)
|
||||
from homeassistant.helpers.config_validation import ( # noqa: F401
|
||||
PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA_BASE,
|
||||
@@ -13,7 +16,6 @@ from homeassistant.helpers.entity_component import EntityComponent
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_AQI = "air_quality_index"
|
||||
ATTR_ATTRIBUTION = "attribution"
|
||||
ATTR_CO2 = "carbon_dioxide"
|
||||
ATTR_CO = "carbon_monoxide"
|
||||
ATTR_N2O = "nitrogen_oxide"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The Airly component."""
|
||||
"""The Airly integration."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
@@ -19,14 +19,13 @@ from .const import (
|
||||
ATTR_API_PM25,
|
||||
ATTR_API_PM25_LIMIT,
|
||||
ATTR_API_PM25_PERCENT,
|
||||
ATTRIBUTION,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
LABEL_ADVICE,
|
||||
MANUFACTURER,
|
||||
)
|
||||
|
||||
ATTRIBUTION = "Data provided by Airly"
|
||||
|
||||
LABEL_ADVICE = "advice"
|
||||
LABEL_AQI_DESCRIPTION = f"{ATTR_AQI}_description"
|
||||
LABEL_AQI_LEVEL = f"{ATTR_AQI}_level"
|
||||
LABEL_PM_2_5_LIMIT = f"{ATTR_PM_2_5}_limit"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Constants for Airly integration."""
|
||||
|
||||
ATTR_API_ADVICE = "ADVICE"
|
||||
ATTR_API_CAQI = "CAQI"
|
||||
ATTR_API_CAQI_DESCRIPTION = "DESCRIPTION"
|
||||
@@ -13,9 +14,15 @@ ATTR_API_PM25_LIMIT = "PM25_LIMIT"
|
||||
ATTR_API_PM25_PERCENT = "PM25_PERCENT"
|
||||
ATTR_API_PRESSURE = "PRESSURE"
|
||||
ATTR_API_TEMPERATURE = "TEMPERATURE"
|
||||
|
||||
ATTR_LABEL = "label"
|
||||
ATTR_UNIT = "unit"
|
||||
|
||||
ATTRIBUTION = "Data provided by Airly"
|
||||
CONF_USE_NEAREST = "use_nearest"
|
||||
DEFAULT_NAME = "Airly"
|
||||
DOMAIN = "airly"
|
||||
LABEL_ADVICE = "advice"
|
||||
MANUFACTURER = "Airly sp. z o.o."
|
||||
MAX_REQUESTS_PER_DAY = 100
|
||||
NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet."
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ICON,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONF_NAME,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
@@ -18,17 +19,14 @@ from .const import (
|
||||
ATTR_API_PM1,
|
||||
ATTR_API_PRESSURE,
|
||||
ATTR_API_TEMPERATURE,
|
||||
ATTR_LABEL,
|
||||
ATTR_UNIT,
|
||||
ATTRIBUTION,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
)
|
||||
|
||||
ATTRIBUTION = "Data provided by Airly"
|
||||
|
||||
ATTR_ICON = "icon"
|
||||
ATTR_LABEL = "label"
|
||||
ATTR_UNIT = "unit"
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
SENSOR_TYPES = {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\uc774 \uc88c\ud45c\uc5d0 \ub300\ud55c Airly \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
|
||||
"already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
|
||||
"wrong_location": "\uc774 \uc9c0\uc5ed\uc5d0\ub294 Airly \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc774 \uc5c6\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"step": {
|
||||
@@ -12,7 +13,7 @@
|
||||
"api_key": "API \ud0a4",
|
||||
"latitude": "\uc704\ub3c4",
|
||||
"longitude": "\uacbd\ub3c4",
|
||||
"name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984"
|
||||
"name": "\uc774\ub984"
|
||||
},
|
||||
"description": "Airly \uacf5\uae30 \ud488\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://developer.airly.eu/register \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694",
|
||||
"title": "Airly"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ICON,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
)
|
||||
@@ -20,7 +21,6 @@ from .const import (
|
||||
|
||||
ATTRIBUTION = "Data provided by AirNow"
|
||||
|
||||
ATTR_ICON = "icon"
|
||||
ATTR_LABEL = "label"
|
||||
ATTR_UNIT = "unit"
|
||||
|
||||
|
||||
21
homeassistant/components/airnow/translations/ko.json
Normal file
21
homeassistant/components/airnow/translations/ko.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
|
||||
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
|
||||
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API \ud0a4",
|
||||
"latitude": "\uc704\ub3c4",
|
||||
"longitude": "\uacbd\ub3c4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
homeassistant/components/airnow/translations/nl.json
Normal file
24
homeassistant/components/airnow/translations/nl.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Apparaat is al geconfigureerd"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Kan geen verbinding maken",
|
||||
"invalid_auth": "Ongeldige authenticatie",
|
||||
"invalid_location": "Geen resultaten gevonden voor die locatie",
|
||||
"unknown": "Onverwachte fout"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API-sleutel",
|
||||
"latitude": "Breedtegraad",
|
||||
"longitude": "Lengtegraad"
|
||||
},
|
||||
"title": "AirNow"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "AirNow"
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
|
||||
"invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
|
||||
"invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
|
||||
"invalid_location": "\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u0432 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e.",
|
||||
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
|
||||
},
|
||||
|
||||
@@ -23,6 +23,7 @@ from homeassistant.const import (
|
||||
CONF_STATE,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
@@ -37,7 +38,7 @@ from .const import (
|
||||
CONF_INTEGRATION_TYPE,
|
||||
DATA_COORDINATOR,
|
||||
DOMAIN,
|
||||
INTEGRATION_TYPE_GEOGRAPHY,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||
INTEGRATION_TYPE_NODE_PRO,
|
||||
LOGGER,
|
||||
)
|
||||
@@ -145,7 +146,7 @@ def _standardize_geography_config_entry(hass, config_entry):
|
||||
# If the config entry data doesn't contain the integration type, add it:
|
||||
entry_updates["data"] = {
|
||||
**config_entry.data,
|
||||
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY,
|
||||
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||
}
|
||||
|
||||
if not entry_updates:
|
||||
@@ -232,7 +233,6 @@ async def async_setup_entry(hass, config_entry):
|
||||
update_method=async_update_data,
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
|
||||
async_sync_geo_coordinator_update_intervals(
|
||||
hass, config_entry.data[CONF_API_KEY]
|
||||
)
|
||||
@@ -262,9 +262,11 @@ async def async_setup_entry(hass, config_entry):
|
||||
update_method=async_update_data,
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
|
||||
|
||||
await coordinator.async_refresh()
|
||||
if not coordinator.last_update_success:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
@@ -299,10 +301,14 @@ async def async_migrate_entry(hass, config_entry):
|
||||
|
||||
# For any geographies that remain, create a new config entry for each one:
|
||||
for geography in geographies:
|
||||
if CONF_LATITUDE in geography:
|
||||
source = "geography_by_coords"
|
||||
else:
|
||||
source = "geography_by_name"
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": "geography"},
|
||||
context={"source": source},
|
||||
data={CONF_API_KEY: config_entry.data[CONF_API_KEY], **geography},
|
||||
)
|
||||
)
|
||||
@@ -327,7 +333,10 @@ async def async_unload_entry(hass, config_entry):
|
||||
remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id)
|
||||
remove_listener()
|
||||
|
||||
if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY:
|
||||
if (
|
||||
config_entry.data[CONF_INTEGRATION_TYPE]
|
||||
== INTEGRATION_TYPE_GEOGRAPHY_COORDS
|
||||
):
|
||||
# Re-calculate the update interval period for any remaining consumers of
|
||||
# this API key:
|
||||
async_sync_geo_coordinator_update_intervals(
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
import asyncio
|
||||
|
||||
from pyairvisual import CloudAPI, NodeSamba
|
||||
from pyairvisual.errors import InvalidKeyError, NodeProError
|
||||
from pyairvisual.errors import (
|
||||
AirVisualError,
|
||||
InvalidKeyError,
|
||||
NodeProError,
|
||||
NotFoundError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
@@ -13,20 +18,46 @@ from homeassistant.const import (
|
||||
CONF_LONGITUDE,
|
||||
CONF_PASSWORD,
|
||||
CONF_SHOW_ON_MAP,
|
||||
CONF_STATE,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
|
||||
from . import async_get_geography_id
|
||||
from .const import ( # pylint: disable=unused-import
|
||||
CONF_GEOGRAPHIES,
|
||||
CONF_CITY,
|
||||
CONF_COUNTRY,
|
||||
CONF_INTEGRATION_TYPE,
|
||||
DOMAIN,
|
||||
INTEGRATION_TYPE_GEOGRAPHY,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_NAME,
|
||||
INTEGRATION_TYPE_NODE_PRO,
|
||||
LOGGER,
|
||||
)
|
||||
|
||||
API_KEY_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): cv.string})
|
||||
GEOGRAPHY_NAME_SCHEMA = API_KEY_DATA_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_CITY): cv.string,
|
||||
vol.Required(CONF_STATE): cv.string,
|
||||
vol.Required(CONF_COUNTRY): cv.string,
|
||||
}
|
||||
)
|
||||
NODE_PRO_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_IP_ADDRESS): str, vol.Required(CONF_PASSWORD): cv.string}
|
||||
)
|
||||
PICK_INTEGRATION_TYPE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("type"): vol.In(
|
||||
[
|
||||
INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_NAME,
|
||||
INTEGRATION_TYPE_NODE_PRO,
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle an AirVisual config flow."""
|
||||
@@ -36,16 +67,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the config flow."""
|
||||
self._entry_data_for_reauth = None
|
||||
self._geo_id = None
|
||||
self._latitude = None
|
||||
self._longitude = None
|
||||
|
||||
self.api_key_data_schema = vol.Schema({vol.Required(CONF_API_KEY): str})
|
||||
|
||||
@property
|
||||
def geography_schema(self):
|
||||
def geography_coords_schema(self):
|
||||
"""Return the data schema for the cloud API."""
|
||||
return self.api_key_data_schema.extend(
|
||||
return API_KEY_DATA_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_LATITUDE, default=self.hass.config.latitude
|
||||
@@ -56,24 +84,72 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def pick_integration_type_schema(self):
|
||||
"""Return the data schema for picking the integration type."""
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required("type"): vol.In(
|
||||
[INTEGRATION_TYPE_GEOGRAPHY, INTEGRATION_TYPE_NODE_PRO]
|
||||
)
|
||||
}
|
||||
async def _async_finish_geography(self, user_input, integration_type):
|
||||
"""Validate a Cloud API key."""
|
||||
websession = aiohttp_client.async_get_clientsession(self.hass)
|
||||
cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession)
|
||||
|
||||
# If this is the first (and only the first) time we've seen this API key, check
|
||||
# that it's valid:
|
||||
valid_keys = self.hass.data.setdefault("airvisual_checked_api_keys", set())
|
||||
valid_keys_lock = self.hass.data.setdefault(
|
||||
"airvisual_checked_api_keys_lock", asyncio.Lock()
|
||||
)
|
||||
|
||||
@property
|
||||
def node_pro_schema(self):
|
||||
"""Return the data schema for a Node/Pro."""
|
||||
return vol.Schema(
|
||||
{vol.Required(CONF_IP_ADDRESS): str, vol.Required(CONF_PASSWORD): str}
|
||||
if integration_type == INTEGRATION_TYPE_GEOGRAPHY_COORDS:
|
||||
coro = cloud_api.air_quality.nearest_city()
|
||||
error_schema = self.geography_coords_schema
|
||||
error_step = "geography_by_coords"
|
||||
else:
|
||||
coro = cloud_api.air_quality.city(
|
||||
user_input[CONF_CITY], user_input[CONF_STATE], user_input[CONF_COUNTRY]
|
||||
)
|
||||
error_schema = GEOGRAPHY_NAME_SCHEMA
|
||||
error_step = "geography_by_name"
|
||||
|
||||
async with valid_keys_lock:
|
||||
if user_input[CONF_API_KEY] not in valid_keys:
|
||||
try:
|
||||
await coro
|
||||
except InvalidKeyError:
|
||||
return self.async_show_form(
|
||||
step_id=error_step,
|
||||
data_schema=error_schema,
|
||||
errors={CONF_API_KEY: "invalid_api_key"},
|
||||
)
|
||||
except NotFoundError:
|
||||
return self.async_show_form(
|
||||
step_id=error_step,
|
||||
data_schema=error_schema,
|
||||
errors={CONF_CITY: "location_not_found"},
|
||||
)
|
||||
except AirVisualError as err:
|
||||
LOGGER.error(err)
|
||||
return self.async_show_form(
|
||||
step_id=error_step,
|
||||
data_schema=error_schema,
|
||||
errors={"base": "unknown"},
|
||||
)
|
||||
|
||||
valid_keys.add(user_input[CONF_API_KEY])
|
||||
|
||||
existing_entry = await self.async_set_unique_id(self._geo_id)
|
||||
if existing_entry:
|
||||
self.hass.config_entries.async_update_entry(existing_entry, data=user_input)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
return self.async_create_entry(
|
||||
title=f"Cloud API ({self._geo_id})",
|
||||
data={**user_input, CONF_INTEGRATION_TYPE: integration_type},
|
||||
)
|
||||
|
||||
async def _async_init_geography(self, user_input, integration_type):
|
||||
"""Handle the initialization of the integration via the cloud API."""
|
||||
self._geo_id = async_get_geography_id(user_input)
|
||||
await self._async_set_unique_id(self._geo_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
return await self._async_finish_geography(user_input, integration_type)
|
||||
|
||||
async def _async_set_unique_id(self, unique_id):
|
||||
"""Set the unique ID of the config flow and abort if it already exists."""
|
||||
await self.async_set_unique_id(unique_id)
|
||||
@@ -85,73 +161,32 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Define the config flow to handle options."""
|
||||
return AirVisualOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_geography(self, user_input=None):
|
||||
"""Handle the initialization of the integration via the cloud API."""
|
||||
async def async_step_geography_by_coords(self, user_input=None):
|
||||
"""Handle the initialization of the cloud API based on latitude/longitude."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="geography", data_schema=self.geography_schema
|
||||
step_id="geography_by_coords", data_schema=self.geography_coords_schema
|
||||
)
|
||||
|
||||
self._geo_id = async_get_geography_id(user_input)
|
||||
await self._async_set_unique_id(self._geo_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
# Find older config entries without unique ID:
|
||||
for entry in self._async_current_entries():
|
||||
if entry.version != 1:
|
||||
continue
|
||||
|
||||
if any(
|
||||
self._geo_id == async_get_geography_id(geography)
|
||||
for geography in entry.data[CONF_GEOGRAPHIES]
|
||||
):
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
return await self.async_step_geography_finish(
|
||||
user_input, "geography", self.geography_schema
|
||||
return await self._async_init_geography(
|
||||
user_input, INTEGRATION_TYPE_GEOGRAPHY_COORDS
|
||||
)
|
||||
|
||||
async def async_step_geography_finish(self, user_input, error_step, error_schema):
|
||||
"""Validate a Cloud API key."""
|
||||
websession = aiohttp_client.async_get_clientsession(self.hass)
|
||||
cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession)
|
||||
async def async_step_geography_by_name(self, user_input=None):
|
||||
"""Handle the initialization of the cloud API based on city/state/country."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="geography_by_name", data_schema=GEOGRAPHY_NAME_SCHEMA
|
||||
)
|
||||
|
||||
# If this is the first (and only the first) time we've seen this API key, check
|
||||
# that it's valid:
|
||||
valid_keys = self.hass.data.setdefault("airvisual_checked_api_keys", set())
|
||||
valid_keys_lock = self.hass.data.setdefault(
|
||||
"airvisual_checked_api_keys_lock", asyncio.Lock()
|
||||
)
|
||||
|
||||
async with valid_keys_lock:
|
||||
if user_input[CONF_API_KEY] not in valid_keys:
|
||||
try:
|
||||
await cloud_api.air_quality.nearest_city()
|
||||
except InvalidKeyError:
|
||||
return self.async_show_form(
|
||||
step_id=error_step,
|
||||
data_schema=error_schema,
|
||||
errors={CONF_API_KEY: "invalid_api_key"},
|
||||
)
|
||||
|
||||
valid_keys.add(user_input[CONF_API_KEY])
|
||||
|
||||
existing_entry = await self.async_set_unique_id(self._geo_id)
|
||||
if existing_entry:
|
||||
self.hass.config_entries.async_update_entry(existing_entry, data=user_input)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
return self.async_create_entry(
|
||||
title=f"Cloud API ({self._geo_id})",
|
||||
data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY},
|
||||
return await self._async_init_geography(
|
||||
user_input, INTEGRATION_TYPE_GEOGRAPHY_NAME
|
||||
)
|
||||
|
||||
async def async_step_node_pro(self, user_input=None):
|
||||
"""Handle the initialization of the integration with a Node/Pro."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="node_pro", data_schema=self.node_pro_schema
|
||||
)
|
||||
return self.async_show_form(step_id="node_pro", data_schema=NODE_PRO_SCHEMA)
|
||||
|
||||
await self._async_set_unique_id(user_input[CONF_IP_ADDRESS])
|
||||
|
||||
@@ -163,7 +198,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
LOGGER.error("Error connecting to Node/Pro unit: %s", err)
|
||||
return self.async_show_form(
|
||||
step_id="node_pro",
|
||||
data_schema=self.node_pro_schema,
|
||||
data_schema=NODE_PRO_SCHEMA,
|
||||
errors={CONF_IP_ADDRESS: "cannot_connect"},
|
||||
)
|
||||
|
||||
@@ -176,39 +211,34 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_reauth(self, data):
|
||||
"""Handle configuration by re-auth."""
|
||||
self._entry_data_for_reauth = data
|
||||
self._geo_id = async_get_geography_id(data)
|
||||
self._latitude = data[CONF_LATITUDE]
|
||||
self._longitude = data[CONF_LONGITUDE]
|
||||
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(self, user_input=None):
|
||||
"""Handle re-auth completion."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm", data_schema=self.api_key_data_schema
|
||||
step_id="reauth_confirm", data_schema=API_KEY_DATA_SCHEMA
|
||||
)
|
||||
|
||||
conf = {
|
||||
CONF_API_KEY: user_input[CONF_API_KEY],
|
||||
CONF_LATITUDE: self._latitude,
|
||||
CONF_LONGITUDE: self._longitude,
|
||||
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY,
|
||||
}
|
||||
conf = {CONF_API_KEY: user_input[CONF_API_KEY], **self._entry_data_for_reauth}
|
||||
|
||||
return await self.async_step_geography_finish(
|
||||
conf, "reauth_confirm", self.api_key_data_schema
|
||||
return await self._async_finish_geography(
|
||||
conf, self._entry_data_for_reauth[CONF_INTEGRATION_TYPE]
|
||||
)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the start of the config flow."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=self.pick_integration_type_schema
|
||||
step_id="user", data_schema=PICK_INTEGRATION_TYPE_SCHEMA
|
||||
)
|
||||
|
||||
if user_input["type"] == INTEGRATION_TYPE_GEOGRAPHY:
|
||||
return await self.async_step_geography()
|
||||
if user_input["type"] == INTEGRATION_TYPE_GEOGRAPHY_COORDS:
|
||||
return await self.async_step_geography_by_coords()
|
||||
if user_input["type"] == INTEGRATION_TYPE_GEOGRAPHY_NAME:
|
||||
return await self.async_step_geography_by_name()
|
||||
return await self.async_step_node_pro()
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import logging
|
||||
DOMAIN = "airvisual"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
INTEGRATION_TYPE_GEOGRAPHY = "Geographical Location"
|
||||
INTEGRATION_TYPE_GEOGRAPHY_COORDS = "Geographical Location by Latitude/Longitude"
|
||||
INTEGRATION_TYPE_GEOGRAPHY_NAME = "Geographical Location by Name"
|
||||
INTEGRATION_TYPE_NODE_PRO = "AirVisual Node/Pro"
|
||||
|
||||
CONF_CITY = "city"
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"""Support for AirVisual air quality sensors."""
|
||||
from logging import getLogger
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
@@ -27,11 +25,10 @@ from .const import (
|
||||
CONF_INTEGRATION_TYPE,
|
||||
DATA_COORDINATOR,
|
||||
DOMAIN,
|
||||
INTEGRATION_TYPE_GEOGRAPHY,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_NAME,
|
||||
)
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
ATTR_CITY = "city"
|
||||
ATTR_COUNTRY = "country"
|
||||
ATTR_POLLUTANT_SYMBOL = "pollutant_symbol"
|
||||
@@ -58,14 +55,23 @@ NODE_PRO_SENSORS = [
|
||||
(SENSOR_KIND_TEMPERATURE, "Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS),
|
||||
]
|
||||
|
||||
POLLUTANT_MAPPING = {
|
||||
"co": {"label": "Carbon Monoxide", "unit": CONCENTRATION_PARTS_PER_MILLION},
|
||||
"n2": {"label": "Nitrogen Dioxide", "unit": CONCENTRATION_PARTS_PER_BILLION},
|
||||
"o3": {"label": "Ozone", "unit": CONCENTRATION_PARTS_PER_BILLION},
|
||||
"p1": {"label": "PM10", "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
"p2": {"label": "PM2.5", "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
"s2": {"label": "Sulfur Dioxide", "unit": CONCENTRATION_PARTS_PER_BILLION},
|
||||
}
|
||||
|
||||
@callback
|
||||
def async_get_pollutant_label(symbol):
|
||||
"""Get a pollutant's label based on its symbol."""
|
||||
if symbol == "co":
|
||||
return "Carbon Monoxide"
|
||||
if symbol == "n2":
|
||||
return "Nitrogen Dioxide"
|
||||
if symbol == "o3":
|
||||
return "Ozone"
|
||||
if symbol == "p1":
|
||||
return "PM10"
|
||||
if symbol == "p2":
|
||||
return "PM2.5"
|
||||
if symbol == "s2":
|
||||
return "Sulfur Dioxide"
|
||||
return symbol
|
||||
|
||||
|
||||
@callback
|
||||
@@ -84,11 +90,32 @@ def async_get_pollutant_level_info(value):
|
||||
return ("Hazardous", "mdi:biohazard")
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_pollutant_unit(symbol):
|
||||
"""Get a pollutant's unit based on its symbol."""
|
||||
if symbol == "co":
|
||||
return CONCENTRATION_PARTS_PER_MILLION
|
||||
if symbol == "n2":
|
||||
return CONCENTRATION_PARTS_PER_BILLION
|
||||
if symbol == "o3":
|
||||
return CONCENTRATION_PARTS_PER_BILLION
|
||||
if symbol == "p1":
|
||||
return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
if symbol == "p2":
|
||||
return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
if symbol == "s2":
|
||||
return CONCENTRATION_PARTS_PER_BILLION
|
||||
return None
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up AirVisual sensors based on a config entry."""
|
||||
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
|
||||
|
||||
if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY:
|
||||
if config_entry.data[CONF_INTEGRATION_TYPE] in [
|
||||
INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_NAME,
|
||||
]:
|
||||
sensors = [
|
||||
AirVisualGeographySensor(
|
||||
coordinator,
|
||||
@@ -173,25 +200,40 @@ class AirVisualGeographySensor(AirVisualEntity):
|
||||
self._state = data[f"aqi{self._locale}"]
|
||||
elif self._kind == SENSOR_KIND_POLLUTANT:
|
||||
symbol = data[f"main{self._locale}"]
|
||||
self._state = POLLUTANT_MAPPING[symbol]["label"]
|
||||
self._state = async_get_pollutant_label(symbol)
|
||||
self._attrs.update(
|
||||
{
|
||||
ATTR_POLLUTANT_SYMBOL: symbol,
|
||||
ATTR_POLLUTANT_UNIT: POLLUTANT_MAPPING[symbol]["unit"],
|
||||
ATTR_POLLUTANT_UNIT: async_get_pollutant_unit(symbol),
|
||||
}
|
||||
)
|
||||
|
||||
if CONF_LATITUDE in self._config_entry.data:
|
||||
if self._config_entry.options[CONF_SHOW_ON_MAP]:
|
||||
self._attrs[ATTR_LATITUDE] = self._config_entry.data[CONF_LATITUDE]
|
||||
self._attrs[ATTR_LONGITUDE] = self._config_entry.data[CONF_LONGITUDE]
|
||||
self._attrs.pop("lati", None)
|
||||
self._attrs.pop("long", None)
|
||||
else:
|
||||
self._attrs["lati"] = self._config_entry.data[CONF_LATITUDE]
|
||||
self._attrs["long"] = self._config_entry.data[CONF_LONGITUDE]
|
||||
self._attrs.pop(ATTR_LATITUDE, None)
|
||||
self._attrs.pop(ATTR_LONGITUDE, None)
|
||||
# Displaying the geography on the map relies upon putting the latitude/longitude
|
||||
# in the entity attributes with "latitude" and "longitude" as the keys.
|
||||
# Conversely, we can hide the location on the map by using other keys, like
|
||||
# "lati" and "long".
|
||||
#
|
||||
# We use any coordinates in the config entry and, in the case of a geography by
|
||||
# name, we fall back to the latitude longitude provided in the coordinator data:
|
||||
latitude = self._config_entry.data.get(
|
||||
CONF_LATITUDE,
|
||||
self.coordinator.data["location"]["coordinates"][1],
|
||||
)
|
||||
longitude = self._config_entry.data.get(
|
||||
CONF_LONGITUDE,
|
||||
self.coordinator.data["location"]["coordinates"][0],
|
||||
)
|
||||
|
||||
if self._config_entry.options[CONF_SHOW_ON_MAP]:
|
||||
self._attrs[ATTR_LATITUDE] = latitude
|
||||
self._attrs[ATTR_LONGITUDE] = longitude
|
||||
self._attrs.pop("lati", None)
|
||||
self._attrs.pop("long", None)
|
||||
else:
|
||||
self._attrs["lati"] = latitude
|
||||
self._attrs["long"] = longitude
|
||||
self._attrs.pop(ATTR_LATITUDE, None)
|
||||
self._attrs.pop(ATTR_LONGITUDE, None)
|
||||
|
||||
|
||||
class AirVisualNodeProSensor(AirVisualEntity):
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"geography": {
|
||||
"geography_by_coords": {
|
||||
"title": "Configure a Geography",
|
||||
"description": "Use the AirVisual cloud API to monitor a geographical location.",
|
||||
"description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"latitude": "[%key:common::config_flow::data::latitude%]",
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]"
|
||||
}
|
||||
},
|
||||
"geography_by_name": {
|
||||
"title": "Configure a Geography",
|
||||
"description": "Use the AirVisual cloud API to monitor a city/state/country.",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"city": "City",
|
||||
"country": "Country",
|
||||
"state": "state"
|
||||
}
|
||||
},
|
||||
"node_pro": {
|
||||
"title": "Configure an AirVisual Node/Pro",
|
||||
"description": "Monitor a personal AirVisual unit. The password can be retrieved from the unit's UI.",
|
||||
@@ -26,17 +36,13 @@
|
||||
},
|
||||
"user": {
|
||||
"title": "Configure AirVisual",
|
||||
"description": "Pick what type of AirVisual data you want to monitor.",
|
||||
"data": {
|
||||
"cloud_api": "Geographical Location",
|
||||
"node_pro": "AirVisual Node Pro",
|
||||
"type": "Integration Type"
|
||||
}
|
||||
"description": "Pick what type of AirVisual data you want to monitor."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"general_error": "[%key:common::config_flow::error::unknown%]",
|
||||
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||
"location_not_found": "Location not found",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"abort": {
|
||||
|
||||
@@ -17,6 +17,20 @@
|
||||
"longitude": "Zem\u011bpisn\u00e1 d\u00e9lka"
|
||||
}
|
||||
},
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "Kl\u00ed\u010d API",
|
||||
"latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka",
|
||||
"longitude": "Zem\u011bpisn\u00e1 d\u00e9lka"
|
||||
}
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "Kl\u00ed\u010d API",
|
||||
"city": "M\u011bsto",
|
||||
"country": "Zem\u011b"
|
||||
}
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Hostitel",
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung fehlgeschlagen",
|
||||
"general_error": "Unerwarteter Fehler",
|
||||
"invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel"
|
||||
"invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel",
|
||||
"location_not_found": "Standort nicht gefunden"
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
@@ -16,8 +17,28 @@
|
||||
"latitude": "Breitengrad",
|
||||
"longitude": "L\u00e4ngengrad"
|
||||
},
|
||||
"description": "Verwende die AirVisual Cloud API, um einen geografischen Standort zu \u00fcberwachen.",
|
||||
"title": "Konfigurieren Sie eine Geografie"
|
||||
},
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "API-Schl\u00fcssel",
|
||||
"latitude": "Breitengrad",
|
||||
"longitude": "L\u00e4ngengrad"
|
||||
},
|
||||
"description": "Verwende die AirVisual Cloud API, um einen L\u00e4ngengrad/Breitengrad zu \u00fcberwachen.",
|
||||
"title": "Konfiguriere einen Standort"
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "API-Schl\u00fcssel",
|
||||
"city": "Stadt",
|
||||
"country": "Land",
|
||||
"state": "Bundesland"
|
||||
},
|
||||
"description": "Verwende die AirVisual Cloud API, um ein(e) Stadt/Bundesland/Land zu \u00fcberwachen.",
|
||||
"title": "Konfiguriere einen Standort"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Host",
|
||||
@@ -29,7 +50,8 @@
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API-Key"
|
||||
}
|
||||
},
|
||||
"title": "AirVisual erneut authentifizieren"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"general_error": "Unexpected error",
|
||||
"invalid_api_key": "Invalid API key"
|
||||
"invalid_api_key": "Invalid API key",
|
||||
"location_not_found": "Location not found"
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
@@ -25,7 +26,17 @@
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude"
|
||||
},
|
||||
"description": "Use the AirVisual cloud API to monitor a geographical location.",
|
||||
"description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
|
||||
"title": "Configure a Geography"
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"city": "City",
|
||||
"country": "Country",
|
||||
"state": "state"
|
||||
},
|
||||
"description": "Use the AirVisual cloud API to monitor a city/state/country.",
|
||||
"title": "Configure a Geography"
|
||||
},
|
||||
"node_pro": {
|
||||
@@ -63,4 +74,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,8 @@
|
||||
"error": {
|
||||
"cannot_connect": "No se pudo conectar",
|
||||
"general_error": "Se ha producido un error desconocido.",
|
||||
"invalid_api_key": "Se proporciona una clave API no v\u00e1lida."
|
||||
"invalid_api_key": "Se proporciona una clave API no v\u00e1lida.",
|
||||
"location_not_found": "Ubicaci\u00f3n no encontrada"
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
@@ -19,6 +20,25 @@
|
||||
"description": "Utilizar la API en la nube de AirVisual para monitorizar una ubicaci\u00f3n geogr\u00e1fica.",
|
||||
"title": "Configurar una Geograf\u00eda"
|
||||
},
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "Clave API",
|
||||
"latitude": "Latitud",
|
||||
"longitude": "Longitud"
|
||||
},
|
||||
"description": "Utilice la API de la nube de AirVisual para supervisar una latitud/longitud.",
|
||||
"title": "Configurar una geograf\u00eda"
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "Clave API",
|
||||
"city": "Ciudad",
|
||||
"country": "Pa\u00eds",
|
||||
"state": "estado"
|
||||
},
|
||||
"description": "Utilice la API de la nube de AirVisual para supervisar una ciudad/estado/pa\u00eds.",
|
||||
"title": "Configurar una geograf\u00eda"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Direcci\u00f3n IP/Nombre de host de la Unidad",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"latitude": "Laiuskraad",
|
||||
"longitude": "Pikkuskraad"
|
||||
},
|
||||
"description": "Kasutage AirVisual pilve API-t geograafilise asukoha j\u00e4lgimiseks.",
|
||||
"description": "Kasuta AirVisual pilve API-t geograafilise asukoha j\u00e4lgimiseks.",
|
||||
"title": "Seadista Geography"
|
||||
},
|
||||
"geography_by_coords": {
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"error": {
|
||||
"cannot_connect": "\u00c9chec de connexion",
|
||||
"general_error": "Erreur inattendue",
|
||||
"invalid_api_key": "Cl\u00e9 API invalide"
|
||||
"invalid_api_key": "Cl\u00e9 API invalide",
|
||||
"location_not_found": "Emplacement introuvable"
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
@@ -19,6 +20,25 @@
|
||||
"description": "Utilisez l'API cloud AirVisual pour surveiller une position g\u00e9ographique.",
|
||||
"title": "Configurer une g\u00e9ographie"
|
||||
},
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "Clef d'API",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude"
|
||||
},
|
||||
"description": "Utilisez l'API cloud AirVisual pour surveiller une latitude / longitude.",
|
||||
"title": "Configurer un lieu g\u00e9ographique"
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "Clef d'API",
|
||||
"city": "Ville",
|
||||
"country": "Pays",
|
||||
"state": "Etat"
|
||||
},
|
||||
"description": "Utilisez l'API cloud AirVisual pour surveiller une ville / un \u00e9tat / un pays.",
|
||||
"title": "Configurer un lieu g\u00e9ographique"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "H\u00f4te",
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "La posizione \u00e8 gi\u00e0 configurata o Node/Pro ID sono gi\u00e0 registrati.",
|
||||
"reauth_successful": "La riautenticazione ha avuto successo"
|
||||
"reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossibile connettersi",
|
||||
"general_error": "Errore imprevisto",
|
||||
"invalid_api_key": "Chiave API non valida"
|
||||
"invalid_api_key": "Chiave API non valida",
|
||||
"location_not_found": "Posizione non trovata"
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
@@ -19,6 +20,25 @@
|
||||
"description": "Utilizzare l'API di AirVisual cloud per monitorare una posizione geografica.",
|
||||
"title": "Configurare una Geografia"
|
||||
},
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "Chiave API",
|
||||
"latitude": "Latitudine",
|
||||
"longitude": "Logitudine"
|
||||
},
|
||||
"description": "Usa l'API cloud di AirVisual per monitorare una latitudine/longitudine.",
|
||||
"title": "Configurare un'area geografica"
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "Chiave API",
|
||||
"city": "Citt\u00e0",
|
||||
"country": "Nazione",
|
||||
"state": "Stato"
|
||||
},
|
||||
"description": "Usa l'API cloud di AirVisual per monitorare una citt\u00e0/stato/paese.",
|
||||
"title": "Configurare un'area geografica"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Host",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\uc88c\ud45c\uac12 \ub610\ub294 Node/Pro ID \uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
|
||||
"already_configured": "Node/Pro ID \uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uac70\ub098 \uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
|
||||
"reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"error": {
|
||||
"general_error": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
|
||||
"invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
|
||||
"general_error": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4",
|
||||
"invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
@@ -17,14 +19,31 @@
|
||||
"description": "AirVisual \ud074\ub77c\uc6b0\ub4dc API \ub97c \uc0ac\uc6a9\ud558\uc5ec \uc9c0\ub9ac\uc801 \uc704\uce58\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.",
|
||||
"title": "\uc9c0\ub9ac\uc801 \uc704\uce58 \uad6c\uc131\ud558\uae30"
|
||||
},
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "API \ud0a4",
|
||||
"latitude": "\uc704\ub3c4",
|
||||
"longitude": "\uacbd\ub3c4"
|
||||
}
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "API \ud0a4"
|
||||
}
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "\uae30\uae30 IP \uc8fc\uc18c/\ud638\uc2a4\ud2b8 \uc774\ub984",
|
||||
"ip_address": "\ud638\uc2a4\ud2b8",
|
||||
"password": "\ube44\ubc00\ubc88\ud638"
|
||||
},
|
||||
"description": "\uc0ac\uc6a9\uc790\uc758 AirVisual \uae30\uae30\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4. \uae30\uae30\uc758 UI \uc5d0\uc11c \ube44\ubc00\ubc88\ud638\ub97c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
|
||||
"title": "AirVisual Node/Pro \uad6c\uc131\ud558\uae30"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API \ud0a4"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"cloud_api": "\uc9c0\ub9ac\uc801 \uc704\uce58",
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Deze co\u00f6rdinaten of Node / Pro ID zijn al geregistreerd."
|
||||
"already_configured": "Deze co\u00f6rdinaten of Node / Pro ID zijn al geregistreerd.",
|
||||
"reauth_successful": "Herauthenticatie was succesvol"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Kan geen verbinding maken",
|
||||
"general_error": "Er is een onbekende fout opgetreden.",
|
||||
"invalid_api_key": "Ongeldige API-sleutel"
|
||||
"invalid_api_key": "Ongeldige API-sleutel",
|
||||
"location_not_found": "Locatie niet gevonden"
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
@@ -18,6 +20,21 @@
|
||||
"description": "Gebruik de AirVisual cloud API om een geografische locatie te bewaken.",
|
||||
"title": "Configureer een geografie"
|
||||
},
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "API-sleutel",
|
||||
"latitude": "Breedtegraad",
|
||||
"longitude": "Lengtegraad"
|
||||
}
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "API-sleutel",
|
||||
"city": "Stad",
|
||||
"country": "Land"
|
||||
},
|
||||
"description": "Gebruik de AirVisual-cloud-API om een stad/staat/land te bewaken."
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "IP adres/hostname van component",
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"error": {
|
||||
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
|
||||
"general_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.",
|
||||
"invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API."
|
||||
"invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.",
|
||||
"location_not_found": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e."
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
@@ -19,6 +20,25 @@
|
||||
"description": "\u041c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e API AirVisual.",
|
||||
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
|
||||
},
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "\u041a\u043b\u044e\u0447 API",
|
||||
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
|
||||
"longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430"
|
||||
},
|
||||
"description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0448\u0438\u0440\u043e\u0442\u044b/\u0434\u043e\u043b\u0433\u043e\u0442\u044b.",
|
||||
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "\u041a\u043b\u044e\u0447 API",
|
||||
"city": "\u0413\u043e\u0440\u043e\u0434",
|
||||
"country": "\u0421\u0442\u0440\u0430\u043d\u0430",
|
||||
"state": "\u0448\u0442\u0430\u0442"
|
||||
},
|
||||
"description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0433\u043e\u0440\u043e\u0434\u0430/\u0448\u0442\u0430\u0442\u0430/\u0441\u0442\u0440\u0430\u043d\u044b.",
|
||||
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "\u0425\u043e\u0441\u0442",
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"data": {
|
||||
"cloud_api": "Geografisk Plats",
|
||||
"type": "Integrationstyp"
|
||||
}
|
||||
},
|
||||
"title": "Konfigurera AirVisual"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,14 +132,12 @@ async def async_attach_trigger(
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
config = TRIGGER_SCHEMA(config)
|
||||
from_state = None
|
||||
|
||||
if config[CONF_TYPE] == "triggered":
|
||||
to_state = STATE_ALARM_TRIGGERED
|
||||
elif config[CONF_TYPE] == "disarmed":
|
||||
to_state = STATE_ALARM_DISARMED
|
||||
elif config[CONF_TYPE] == "arming":
|
||||
from_state = STATE_ALARM_DISARMED
|
||||
to_state = STATE_ALARM_ARMING
|
||||
elif config[CONF_TYPE] == "armed_home":
|
||||
to_state = STATE_ALARM_ARMED_HOME
|
||||
@@ -153,8 +151,6 @@ async def async_attach_trigger(
|
||||
CONF_ENTITY_ID: config[CONF_ENTITY_ID],
|
||||
state_trigger.CONF_TO: to_state,
|
||||
}
|
||||
if from_state:
|
||||
state_config[state_trigger.CONF_FROM] = from_state
|
||||
state_config = state_trigger.TRIGGER_SCHEMA(state_config)
|
||||
return await state_trigger.async_attach_trigger(
|
||||
hass, state_config, action, automation_info, platform_type="device"
|
||||
|
||||
@@ -1,61 +1,74 @@
|
||||
# Describes the format for available alarm control panel services
|
||||
|
||||
alarm_disarm:
|
||||
name: Disarm
|
||||
description: Send the alarm the command for disarm.
|
||||
target:
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to disarm.
|
||||
example: "alarm_control_panel.downstairs"
|
||||
code:
|
||||
name: Code
|
||||
description: An optional code to disarm the alarm control panel with.
|
||||
example: "1234"
|
||||
selector:
|
||||
text:
|
||||
|
||||
alarm_arm_custom_bypass:
|
||||
name: Arm with custom bypass
|
||||
description: Send arm custom bypass command.
|
||||
target:
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm custom bypass.
|
||||
example: "alarm_control_panel.downstairs"
|
||||
code:
|
||||
description: An optional code to arm custom bypass the alarm control panel with.
|
||||
name: Code
|
||||
description:
|
||||
An optional code to arm custom bypass the alarm control panel with.
|
||||
example: "1234"
|
||||
selector:
|
||||
text:
|
||||
|
||||
alarm_arm_home:
|
||||
name: Arm home
|
||||
description: Send the alarm the command for arm home.
|
||||
target:
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm home.
|
||||
example: "alarm_control_panel.downstairs"
|
||||
code:
|
||||
name: Code
|
||||
description: An optional code to arm home the alarm control panel with.
|
||||
example: "1234"
|
||||
selector:
|
||||
text:
|
||||
|
||||
alarm_arm_away:
|
||||
name: Arm away
|
||||
description: Send the alarm the command for arm away.
|
||||
target:
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm away.
|
||||
example: "alarm_control_panel.downstairs"
|
||||
code:
|
||||
name: Code
|
||||
description: An optional code to arm away the alarm control panel with.
|
||||
example: "1234"
|
||||
selector:
|
||||
text:
|
||||
|
||||
alarm_arm_night:
|
||||
name: Arm night
|
||||
description: Send the alarm the command for arm night.
|
||||
target:
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm night.
|
||||
example: "alarm_control_panel.downstairs"
|
||||
code:
|
||||
name: Code
|
||||
description: An optional code to arm night the alarm control panel with.
|
||||
example: "1234"
|
||||
selector:
|
||||
text:
|
||||
|
||||
alarm_trigger:
|
||||
name: Trigger
|
||||
description: Send the alarm the command for trigger.
|
||||
target:
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to trigger.
|
||||
example: "alarm_control_panel.downstairs"
|
||||
code:
|
||||
name: Code
|
||||
description: An optional code to trigger the alarm control panel with.
|
||||
example: "1234"
|
||||
selector:
|
||||
text:
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\uc7a5\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4."
|
||||
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "AlarmDecoder\uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"protocol": {
|
||||
"data": {
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_NAME,
|
||||
CONF_REPEAT,
|
||||
CONF_STATE,
|
||||
SERVICE_TOGGLE,
|
||||
SERVICE_TURN_OFF,
|
||||
@@ -33,7 +34,6 @@ DOMAIN = "alert"
|
||||
|
||||
CONF_CAN_ACK = "can_acknowledge"
|
||||
CONF_NOTIFIERS = "notifiers"
|
||||
CONF_REPEAT = "repeat"
|
||||
CONF_SKIP_FIRST = "skip_first"
|
||||
CONF_ALERT_MESSAGE = "message"
|
||||
CONF_DONE_MESSAGE = "done_message"
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
toggle:
|
||||
name: Toggle
|
||||
description: Toggle alert's notifications.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alert to toggle.
|
||||
example: alert.garage_door_open
|
||||
target:
|
||||
|
||||
turn_off:
|
||||
name: Turn off
|
||||
description: Silence alert's notifications.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alert to silence.
|
||||
example: alert.garage_door_open
|
||||
target:
|
||||
|
||||
turn_on:
|
||||
name: Turn on
|
||||
description: Reset alert's notifications.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alert to reset.
|
||||
example: alert.garage_door_open
|
||||
target:
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
"""Support for Alexa skill service end point."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_NAME
|
||||
from homeassistant.const import (
|
||||
CONF_CLIENT_ID,
|
||||
CONF_CLIENT_SECRET,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, entityfilter
|
||||
|
||||
from . import flash_briefings, intent, smart_home_http
|
||||
@@ -14,7 +19,6 @@ from .const import (
|
||||
CONF_ENTITY_CONFIG,
|
||||
CONF_FILTER,
|
||||
CONF_LOCALE,
|
||||
CONF_PASSWORD,
|
||||
CONF_SUPPORTED_LOCALES,
|
||||
CONF_TEXT,
|
||||
CONF_TITLE,
|
||||
|
||||
@@ -46,7 +46,6 @@ from .const import (
|
||||
API_THERMOSTAT_MODES,
|
||||
API_THERMOSTAT_PRESETS,
|
||||
DATE_FORMAT,
|
||||
PERCENTAGE_FAN_MAP,
|
||||
Inputs,
|
||||
)
|
||||
from .errors import UnsupportedProperty
|
||||
@@ -668,9 +667,7 @@ class AlexaPercentageController(AlexaCapability):
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
if self.entity.domain == fan.DOMAIN:
|
||||
speed = self.entity.attributes.get(fan.ATTR_SPEED)
|
||||
|
||||
return PERCENTAGE_FAN_MAP.get(speed, 0)
|
||||
return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
|
||||
|
||||
if self.entity.domain == cover.DOMAIN:
|
||||
return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION, 0)
|
||||
@@ -1155,9 +1152,7 @@ class AlexaPowerLevelController(AlexaCapability):
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
if self.entity.domain == fan.DOMAIN:
|
||||
speed = self.entity.attributes.get(fan.ATTR_SPEED)
|
||||
|
||||
return PERCENTAGE_FAN_MAP.get(speed)
|
||||
return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Constants for the Alexa integration."""
|
||||
from collections import OrderedDict
|
||||
|
||||
from homeassistant.components import fan
|
||||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
|
||||
@@ -19,7 +18,6 @@ CONF_FILTER = "filter"
|
||||
CONF_ENTITY_CONFIG = "entity_config"
|
||||
CONF_ENDPOINT = "endpoint"
|
||||
CONF_LOCALE = "locale"
|
||||
CONF_PASSWORD = "password"
|
||||
|
||||
ATTR_UID = "uid"
|
||||
ATTR_UPDATE_DATE = "updateDate"
|
||||
@@ -53,10 +51,13 @@ CONF_SUPPORTED_LOCALES = (
|
||||
"en-US",
|
||||
"es-ES",
|
||||
"es-MX",
|
||||
"es-US",
|
||||
"fr-CA",
|
||||
"fr-FR",
|
||||
"hi-IN",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
"pt-BR",
|
||||
)
|
||||
|
||||
API_TEMP_UNITS = {TEMP_FAHRENHEIT: "FAHRENHEIT", TEMP_CELSIUS: "CELSIUS"}
|
||||
@@ -78,13 +79,6 @@ API_THERMOSTAT_MODES = OrderedDict(
|
||||
API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"}
|
||||
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
|
||||
|
||||
PERCENTAGE_FAN_MAP = {
|
||||
fan.SPEED_OFF: 0,
|
||||
fan.SPEED_LOW: 33,
|
||||
fan.SPEED_MEDIUM: 66,
|
||||
fan.SPEED_HIGH: 100,
|
||||
}
|
||||
|
||||
|
||||
class Cause:
|
||||
"""Possible causes for property changes.
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
import uuid
|
||||
|
||||
from homeassistant.components import http
|
||||
from homeassistant.const import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED
|
||||
from homeassistant.const import CONF_PASSWORD, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import template
|
||||
import homeassistant.util.dt as dt_util
|
||||
@@ -20,7 +20,6 @@ from .const import (
|
||||
ATTR_UPDATE_DATE,
|
||||
CONF_AUDIO,
|
||||
CONF_DISPLAY_URL,
|
||||
CONF_PASSWORD,
|
||||
CONF_TEXT,
|
||||
CONF_TITLE,
|
||||
CONF_UID,
|
||||
|
||||
@@ -54,7 +54,6 @@ from .const import (
|
||||
API_THERMOSTAT_MODES,
|
||||
API_THERMOSTAT_MODES_CUSTOM,
|
||||
API_THERMOSTAT_PRESETS,
|
||||
PERCENTAGE_FAN_MAP,
|
||||
Cause,
|
||||
Inputs,
|
||||
)
|
||||
@@ -360,17 +359,9 @@ async def async_api_set_percentage(hass, config, directive, context):
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
speed = "off"
|
||||
|
||||
service = fan.SERVICE_SET_PERCENTAGE
|
||||
percentage = int(directive.payload["percentage"])
|
||||
if percentage <= 33:
|
||||
speed = "low"
|
||||
elif percentage <= 66:
|
||||
speed = "medium"
|
||||
elif percentage <= 100:
|
||||
speed = "high"
|
||||
data[fan.ATTR_SPEED] = speed
|
||||
data[fan.ATTR_PERCENTAGE] = percentage
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False, context=context
|
||||
@@ -388,22 +379,12 @@ async def async_api_adjust_percentage(hass, config, directive, context):
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
speed = entity.attributes.get(fan.ATTR_SPEED)
|
||||
current = PERCENTAGE_FAN_MAP.get(speed, 100)
|
||||
service = fan.SERVICE_SET_PERCENTAGE
|
||||
current = entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
|
||||
|
||||
# set percentage
|
||||
percentage = max(0, percentage_delta + current)
|
||||
speed = "off"
|
||||
|
||||
if percentage <= 33:
|
||||
speed = "low"
|
||||
elif percentage <= 66:
|
||||
speed = "medium"
|
||||
elif percentage <= 100:
|
||||
speed = "high"
|
||||
|
||||
data[fan.ATTR_SPEED] = speed
|
||||
percentage = min(100, max(0, percentage_delta + current))
|
||||
data[fan.ATTR_PERCENTAGE] = percentage
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False, context=context
|
||||
@@ -854,18 +835,9 @@ async def async_api_set_power_level(hass, config, directive, context):
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
speed = "off"
|
||||
|
||||
service = fan.SERVICE_SET_PERCENTAGE
|
||||
percentage = int(directive.payload["powerLevel"])
|
||||
if percentage <= 33:
|
||||
speed = "low"
|
||||
elif percentage <= 66:
|
||||
speed = "medium"
|
||||
else:
|
||||
speed = "high"
|
||||
|
||||
data[fan.ATTR_SPEED] = speed
|
||||
data[fan.ATTR_PERCENTAGE] = percentage
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False, context=context
|
||||
@@ -883,22 +855,12 @@ async def async_api_adjust_power_level(hass, config, directive, context):
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
speed = entity.attributes.get(fan.ATTR_SPEED)
|
||||
current = PERCENTAGE_FAN_MAP.get(speed, 100)
|
||||
service = fan.SERVICE_SET_PERCENTAGE
|
||||
current = entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
|
||||
|
||||
# set percentage
|
||||
percentage = max(0, percentage_delta + current)
|
||||
speed = "off"
|
||||
|
||||
if percentage <= 33:
|
||||
speed = "low"
|
||||
elif percentage <= 66:
|
||||
speed = "medium"
|
||||
else:
|
||||
speed = "high"
|
||||
|
||||
data[fan.ATTR_SPEED] = speed
|
||||
percentage = min(100, max(0, percentage_delta + current))
|
||||
data[fan.ATTR_PERCENTAGE] = percentage
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False, context=context
|
||||
|
||||
@@ -8,7 +8,7 @@ import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.const import HTTP_ACCEPTED, MATCH_ALL, STATE_ON
|
||||
from homeassistant.core import State
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.helpers.significant_change import create_checker
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
@@ -28,7 +28,20 @@ async def async_enable_proactive_mode(hass, smart_home_config):
|
||||
# Validate we can get access token.
|
||||
await smart_home_config.async_get_access_token()
|
||||
|
||||
checker = await create_checker(hass, DOMAIN)
|
||||
@callback
|
||||
def extra_significant_check(
|
||||
hass: HomeAssistant,
|
||||
old_state: str,
|
||||
old_attrs: dict,
|
||||
old_extra_arg: dict,
|
||||
new_state: str,
|
||||
new_attrs: dict,
|
||||
new_extra_arg: dict,
|
||||
):
|
||||
"""Check if the serialized data has changed."""
|
||||
return old_extra_arg is not None and old_extra_arg != new_extra_arg
|
||||
|
||||
checker = await create_checker(hass, DOMAIN, extra_significant_check)
|
||||
|
||||
async def async_entity_state_listener(
|
||||
changed_entity: str,
|
||||
@@ -70,15 +83,22 @@ async def async_enable_proactive_mode(hass, smart_home_config):
|
||||
if not should_report and not should_doorbell:
|
||||
return
|
||||
|
||||
if not checker.async_is_significant_change(new_state):
|
||||
return
|
||||
|
||||
if should_doorbell:
|
||||
should_report = False
|
||||
|
||||
if should_report:
|
||||
alexa_properties = list(alexa_changed_entity.serialize_properties())
|
||||
else:
|
||||
alexa_properties = None
|
||||
|
||||
if not checker.async_is_significant_change(
|
||||
new_state, extra_arg=alexa_properties
|
||||
):
|
||||
return
|
||||
|
||||
if should_report:
|
||||
await async_send_changereport_message(
|
||||
hass, smart_home_config, alexa_changed_entity
|
||||
hass, smart_home_config, alexa_changed_entity, alexa_properties
|
||||
)
|
||||
|
||||
elif should_doorbell:
|
||||
@@ -92,7 +112,7 @@ async def async_enable_proactive_mode(hass, smart_home_config):
|
||||
|
||||
|
||||
async def async_send_changereport_message(
|
||||
hass, config, alexa_entity, *, invalidate_access_token=True
|
||||
hass, config, alexa_entity, alexa_properties, *, invalidate_access_token=True
|
||||
):
|
||||
"""Send a ChangeReport message for an Alexa entity.
|
||||
|
||||
@@ -107,7 +127,7 @@ async def async_send_changereport_message(
|
||||
payload = {
|
||||
API_CHANGE: {
|
||||
"cause": {"type": Cause.APP_INTERACTION},
|
||||
"properties": list(alexa_entity.serialize_properties()),
|
||||
"properties": alexa_properties,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +166,7 @@ async def async_send_changereport_message(
|
||||
):
|
||||
config.async_invalidate_access_token()
|
||||
return await async_send_changereport_message(
|
||||
hass, config, alexa_entity, invalidate_access_token=False
|
||||
hass, config, alexa_entity, alexa_properties, invalidate_access_token=False
|
||||
)
|
||||
|
||||
_LOGGER.error(
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"cannot_connect": "Almond \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
|
||||
"missing_configuration": "Almond \uc124\uc815 \ubc29\ubc95\uc5d0 \ub300\ud55c \uc124\uba85\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694.",
|
||||
"no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})"
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
|
||||
"missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
|
||||
"no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
|
||||
"single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
|
||||
@@ -6,6 +6,7 @@ import botocore
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.tts import PLATFORM_SCHEMA, Provider
|
||||
from homeassistant.const import ATTR_CREDENTIALS, CONF_PROFILE_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -13,8 +14,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
CONF_REGION = "region_name"
|
||||
CONF_ACCESS_KEY_ID = "aws_access_key_id"
|
||||
CONF_SECRET_ACCESS_KEY = "aws_secret_access_key"
|
||||
CONF_PROFILE_NAME = "profile_name"
|
||||
ATTR_CREDENTIALS = "credentials"
|
||||
|
||||
DEFAULT_REGION = "us-east-1"
|
||||
SUPPORTED_REGIONS = [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user