mirror of
https://github.com/home-assistant/core.git
synced 2026-01-06 23:57:17 +01:00
Compare commits
595 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c78d3b6154 | ||
|
|
dcfe7b2280 | ||
|
|
6330bb1004 | ||
|
|
672a896124 | ||
|
|
567e3e6e50 | ||
|
|
259915eee9 | ||
|
|
d7859b5900 | ||
|
|
ebcae2503c | ||
|
|
77ee2f1f3e | ||
|
|
761385dea1 | ||
|
|
2b2809a4c6 | ||
|
|
3c32bfda95 | ||
|
|
65d9460e09 | ||
|
|
2b542b7789 | ||
|
|
bbdb7a6f4c | ||
|
|
e049b35413 | ||
|
|
cdcc535ae1 | ||
|
|
4662ab215c | ||
|
|
80aa2075c6 | ||
|
|
4b7d944a74 | ||
|
|
b7218e6a1d | ||
|
|
6d0ac30687 | ||
|
|
0ceace96e7 | ||
|
|
ec7f2657cd | ||
|
|
5945929e7e | ||
|
|
9667c8057f | ||
|
|
028cc8d24f | ||
|
|
df47a8c58c | ||
|
|
657544a381 | ||
|
|
143eb4e8f4 | ||
|
|
41d2321756 | ||
|
|
f02e887fcc | ||
|
|
97e8e20bcc | ||
|
|
33ed113211 | ||
|
|
ba2b28cd4d | ||
|
|
d843bf9c58 | ||
|
|
84d6453a97 | ||
|
|
1f54edfbc4 | ||
|
|
27be95e597 | ||
|
|
d2dec44b18 | ||
|
|
722d285904 | ||
|
|
bc8aa73448 | ||
|
|
b557157ea1 | ||
|
|
85404783d6 | ||
|
|
06440bf076 | ||
|
|
84a2e5d8fb | ||
|
|
9bb7e40ee3 | ||
|
|
368682647d | ||
|
|
1d86905d5b | ||
|
|
8748ace244 | ||
|
|
5e73846bcc | ||
|
|
b5bfc759ec | ||
|
|
075b575bde | ||
|
|
29c6584fe2 | ||
|
|
48127cade0 | ||
|
|
c8efbb2cdc | ||
|
|
9d112dc3f0 | ||
|
|
11c78d5de8 | ||
|
|
bc30491dc0 | ||
|
|
fe93ea9bdf | ||
|
|
75fa9b2fba | ||
|
|
78da6828f0 | ||
|
|
19e19009cc | ||
|
|
1e784b4d7a | ||
|
|
c218757336 | ||
|
|
25f6302813 | ||
|
|
4b3d4b275e | ||
|
|
f36755e447 | ||
|
|
9fd21d20ae | ||
|
|
cd6679eb5b | ||
|
|
1b79872dd6 | ||
|
|
732743aeb5 | ||
|
|
0ec1401be7 | ||
|
|
cc166bf6a7 | ||
|
|
11602c1da0 | ||
|
|
a3f0d55737 | ||
|
|
336b6adc88 | ||
|
|
5b53bd6aa0 | ||
|
|
5fd1053a38 | ||
|
|
80bc42af4f | ||
|
|
c8d885fb78 | ||
|
|
e73569c203 | ||
|
|
0f3b6f1739 | ||
|
|
af2949f85f | ||
|
|
b3886820b4 | ||
|
|
d717d9f6be | ||
|
|
f225570980 | ||
|
|
e505a9b7b4 | ||
|
|
ef79566864 | ||
|
|
fff3cb0b46 | ||
|
|
5652a4a58b | ||
|
|
cb9e0c03d5 | ||
|
|
d6d28dd3e9 | ||
|
|
eb610e6093 | ||
|
|
7db28d3d91 | ||
|
|
452d7cfd61 | ||
|
|
7f3871028d | ||
|
|
e476949c3e | ||
|
|
9036aafc81 | ||
|
|
2a2318b7f6 | ||
|
|
b75356d532 | ||
|
|
0f92d061c4 | ||
|
|
3b83a64f7c | ||
|
|
db87842335 | ||
|
|
798f630029 | ||
|
|
96b8c517f0 | ||
|
|
4af4b2d10e | ||
|
|
2339cb05ad | ||
|
|
aae6ff830a | ||
|
|
1c11394f5f | ||
|
|
162e2b8385 | ||
|
|
e295ca7b8e | ||
|
|
3e325a4ef9 | ||
|
|
0007f35f96 | ||
|
|
4e020b90e1 | ||
|
|
04636e9ba7 | ||
|
|
218c82eaf3 | ||
|
|
22c0733d8e | ||
|
|
d3f2854c89 | ||
|
|
eabc7b22cd | ||
|
|
012e91f9b1 | ||
|
|
6395087a40 | ||
|
|
b3580f46b9 | ||
|
|
3bdee57066 | ||
|
|
2208563de4 | ||
|
|
418fa226e6 | ||
|
|
ba21608042 | ||
|
|
7676b3fbe8 | ||
|
|
5ab3c7b765 | ||
|
|
6cba51fd0e | ||
|
|
fe148606b8 | ||
|
|
574669bd20 | ||
|
|
83c5dc67f7 | ||
|
|
2ffadde0a3 | ||
|
|
7dac7b9e5e | ||
|
|
d820efc4e3 | ||
|
|
8755389c49 | ||
|
|
11647f9fab | ||
|
|
578bfe9798 | ||
|
|
8c27bf8c7c | ||
|
|
ab4e1fddd5 | ||
|
|
b9a488912a | ||
|
|
199db7219e | ||
|
|
937688f7a6 | ||
|
|
25408bd483 | ||
|
|
a65d14c0cd | ||
|
|
12d16d9bdc | ||
|
|
14dd8791ec | ||
|
|
088584b66d | ||
|
|
963ffa1ccc | ||
|
|
990e7c57f2 | ||
|
|
0979ce476a | ||
|
|
ded37d971d | ||
|
|
7a83b86ebd | ||
|
|
8ef2f1f67b | ||
|
|
99c2e4ac44 | ||
|
|
fe92cf1e72 | ||
|
|
7dbbea2238 | ||
|
|
7be015fcc6 | ||
|
|
a8f22287ca | ||
|
|
b3a08d5876 | ||
|
|
e2f55a959f | ||
|
|
b81260e912 | ||
|
|
7be197b845 | ||
|
|
fd21d6cc9d | ||
|
|
3f65a03024 | ||
|
|
82c6d3d8c2 | ||
|
|
71eaef8da4 | ||
|
|
d812f23f6b | ||
|
|
17dce6697f | ||
|
|
49cfebd903 | ||
|
|
2228f2ef66 | ||
|
|
734d8c52e9 | ||
|
|
caf0751be8 | ||
|
|
7b81727c69 | ||
|
|
97394df0b9 | ||
|
|
c3e9bd1444 | ||
|
|
31d92683f7 | ||
|
|
cee51ecb2b | ||
|
|
646aaab936 | ||
|
|
4c1eeb9e96 | ||
|
|
ca460ace5d | ||
|
|
2be0d1b096 | ||
|
|
47f64b472d | ||
|
|
96d20a64d5 | ||
|
|
4d187e08d4 | ||
|
|
4d52adb008 | ||
|
|
6c29315088 | ||
|
|
6403a13ea3 | ||
|
|
de76b59d0b | ||
|
|
616f23ae1d | ||
|
|
d859c3fa86 | ||
|
|
e753ffca94 | ||
|
|
c44f5d31ef | ||
|
|
e6a2c18430 | ||
|
|
8b49ecbe7d | ||
|
|
01eee52990 | ||
|
|
70a8cac19d | ||
|
|
9aed40a88d | ||
|
|
7641658e38 | ||
|
|
8cf595dd3e | ||
|
|
f17d6c0593 | ||
|
|
5cab319798 | ||
|
|
4394e37df9 | ||
|
|
64b4c8f43a | ||
|
|
a3d05328ec | ||
|
|
2bdbf6955d | ||
|
|
23382ab199 | ||
|
|
8a3d43745d | ||
|
|
6b56985e01 | ||
|
|
ed41421a3d | ||
|
|
f019131352 | ||
|
|
4ec313cb3b | ||
|
|
68e33fdbf5 | ||
|
|
312ad7057d | ||
|
|
0d49b19624 | ||
|
|
6d9c37d636 | ||
|
|
ed881f399f | ||
|
|
3a466195b9 | ||
|
|
3453d31f01 | ||
|
|
afa0d37ff0 | ||
|
|
57c96a5489 | ||
|
|
6d9fda04ac | ||
|
|
b4c657a39c | ||
|
|
35cb0458fa | ||
|
|
6d3343e4d1 | ||
|
|
f73bda1218 | ||
|
|
c29bffc8d8 | ||
|
|
cc6e70a270 | ||
|
|
42821b5f64 | ||
|
|
c164533404 | ||
|
|
acdf9c7ce2 | ||
|
|
0cea54cea1 | ||
|
|
203701bc7c | ||
|
|
44f6151548 | ||
|
|
1a5fe3d880 | ||
|
|
a62e514d8f | ||
|
|
f0f386e314 | ||
|
|
bb37cf906c | ||
|
|
406b45c6e7 | ||
|
|
377b129c9c | ||
|
|
410f19c777 | ||
|
|
4bbfc04f5e | ||
|
|
c7a32e59b7 | ||
|
|
fb9aad8791 | ||
|
|
493d2743ba | ||
|
|
f259c5724b | ||
|
|
ea8bb28d21 | ||
|
|
8aa136f7ed | ||
|
|
4905f4dd97 | ||
|
|
45fae5a50e | ||
|
|
2eec2cc656 | ||
|
|
a57aae9891 | ||
|
|
9cdfa77a21 | ||
|
|
0af635e8d7 | ||
|
|
08ac6da8a6 | ||
|
|
8701be095b | ||
|
|
0b57cfb004 | ||
|
|
ddeb7f3bea | ||
|
|
44c619a853 | ||
|
|
d8370f44cb | ||
|
|
dd75c49796 | ||
|
|
8b232e7ce6 | ||
|
|
3c465434cd | ||
|
|
e30c324b32 | ||
|
|
903c86a116 | ||
|
|
c96778c82a | ||
|
|
1e18a2c679 | ||
|
|
5b35317e1e | ||
|
|
bf4830bc07 | ||
|
|
3ffa0176cc | ||
|
|
ccbc231d3a | ||
|
|
dee229152f | ||
|
|
76c30aca38 | ||
|
|
3d0c3ab746 | ||
|
|
32faf5b709 | ||
|
|
09ff272290 | ||
|
|
3a5ba77e04 | ||
|
|
68723730a7 | ||
|
|
0125b3fd80 | ||
|
|
fb5b5223fb | ||
|
|
aacf7ba9aa | ||
|
|
bf29824dac | ||
|
|
a1cb4018a1 | ||
|
|
c7700ad11c | ||
|
|
ed8f89df74 | ||
|
|
65c7bdc1ad | ||
|
|
bf40bea965 | ||
|
|
ef180c489a | ||
|
|
6fb8378b45 | ||
|
|
27a9f5a05c | ||
|
|
16ab799798 | ||
|
|
03488af3fb | ||
|
|
dbb3802b4e | ||
|
|
ead38f6005 | ||
|
|
c2525bede2 | ||
|
|
b79057348d | ||
|
|
6b18b92bdd | ||
|
|
ada0f7cf65 | ||
|
|
87a0118082 | ||
|
|
688bdc6532 | ||
|
|
bba9ef7d7d | ||
|
|
635252ec8e | ||
|
|
a10ca95c01 | ||
|
|
4244ea78d0 | ||
|
|
5aa2bd81cf | ||
|
|
61d5b3028d | ||
|
|
b9f4a7220e | ||
|
|
2ea53e0787 | ||
|
|
7c302bfd7e | ||
|
|
ff80fc347b | ||
|
|
4b541f4058 | ||
|
|
855274e354 | ||
|
|
43eaa960e8 | ||
|
|
81a0ce621e | ||
|
|
18d36e011a | ||
|
|
6d44245456 | ||
|
|
4b90ed6b22 | ||
|
|
cc8b811572 | ||
|
|
faeee4f7ad | ||
|
|
9aa6037219 | ||
|
|
d0742cb332 | ||
|
|
e096532cf1 | ||
|
|
25e5864a22 | ||
|
|
338077f557 | ||
|
|
f925d9ca6b | ||
|
|
b1c9f8d55d | ||
|
|
32eb4e518b | ||
|
|
9928b977fd | ||
|
|
dc9da79a1c | ||
|
|
2ba86310f0 | ||
|
|
457708cbda | ||
|
|
82d6fe5bd5 | ||
|
|
dae4543e54 | ||
|
|
33c5e09ac2 | ||
|
|
f09cea1499 | ||
|
|
14c39f7c24 | ||
|
|
b83a405b14 | ||
|
|
699a38de52 | ||
|
|
fe14be53e3 | ||
|
|
b32e6fe0d5 | ||
|
|
d05450487c | ||
|
|
f9aa364b6d | ||
|
|
5eab4f1dcc | ||
|
|
4c59a6522a | ||
|
|
40bb4266c9 | ||
|
|
bf8b201bb3 | ||
|
|
cd0da4ed0e | ||
|
|
22acc03fb8 | ||
|
|
10831a0889 | ||
|
|
5de4f546f9 | ||
|
|
2efa297df1 | ||
|
|
98229899dc | ||
|
|
0a792620f8 | ||
|
|
54f6cfd569 | ||
|
|
70fff26383 | ||
|
|
dc11e41cf0 | ||
|
|
a6e091f60f | ||
|
|
1428919f98 | ||
|
|
6f9943787a | ||
|
|
796b195c73 | ||
|
|
47f8d248f7 | ||
|
|
b9a0f40827 | ||
|
|
6b204941cf | ||
|
|
4d62e77049 | ||
|
|
b80bed64f5 | ||
|
|
18b7f74ad7 | ||
|
|
b2081c579b | ||
|
|
bef85ecd2e | ||
|
|
e0f50a9e54 | ||
|
|
a8797a08c6 | ||
|
|
edb7ec78f9 | ||
|
|
4a1da0b041 | ||
|
|
0b84eefa2e | ||
|
|
50888ae339 | ||
|
|
a9f796a97c | ||
|
|
10ff169c76 | ||
|
|
0b22880f22 | ||
|
|
5a4e6bbb07 | ||
|
|
0776456b59 | ||
|
|
01fc322488 | ||
|
|
2a2af80309 | ||
|
|
2765440aa5 | ||
|
|
43e174899d | ||
|
|
07b6aaec63 | ||
|
|
ef53a2d118 | ||
|
|
1099018a5e | ||
|
|
4bdb21a871 | ||
|
|
6880be5aeb | ||
|
|
f0e187e306 | ||
|
|
7c5ac88aae | ||
|
|
54c57fe5db | ||
|
|
b444dfe8a6 | ||
|
|
fb226e3e3b | ||
|
|
3a3d488de3 | ||
|
|
30841ef4da | ||
|
|
501b3f9927 | ||
|
|
5efc61feaf | ||
|
|
28abc30b4d | ||
|
|
0471e15c28 | ||
|
|
ec28ee3c42 | ||
|
|
1281da024c | ||
|
|
c789f11ef8 | ||
|
|
dbd5396dc7 | ||
|
|
71900ca719 | ||
|
|
c15445159d | ||
|
|
dd885a456e | ||
|
|
b5c9eca654 | ||
|
|
dcf925a67f | ||
|
|
d42d8543c8 | ||
|
|
fa0185a481 | ||
|
|
28d2f9bd87 | ||
|
|
27ea59f6c3 | ||
|
|
ae776e2d28 | ||
|
|
fed5d0f5be | ||
|
|
4f134f339c | ||
|
|
264d18bc83 | ||
|
|
4c1d978aa4 | ||
|
|
196fe4b927 | ||
|
|
a9de9aa58d | ||
|
|
e874093818 | ||
|
|
c71a6ee562 | ||
|
|
57ee514d70 | ||
|
|
4692605974 | ||
|
|
23a579421d | ||
|
|
1568de62df | ||
|
|
8cec559103 | ||
|
|
258fe1f09b | ||
|
|
e5487722a8 | ||
|
|
7f0dd442fd | ||
|
|
ef6c39f911 | ||
|
|
7317b1bb8b | ||
|
|
c0ae7b1a49 | ||
|
|
686a856a17 | ||
|
|
51e6371991 | ||
|
|
96c233d4b9 | ||
|
|
17fbeb6245 | ||
|
|
c59e049050 | ||
|
|
6c64b315db | ||
|
|
2f6ef08959 | ||
|
|
9c8e10936b | ||
|
|
f2c7e3fed4 | ||
|
|
6adbf3ba84 | ||
|
|
da10598fa1 | ||
|
|
6c8ed86f3e | ||
|
|
f1005d37a7 | ||
|
|
d270d52cb5 | ||
|
|
6e26713184 | ||
|
|
e9c19462d6 | ||
|
|
57ccd8283d | ||
|
|
4ffacec4be | ||
|
|
44bf5ba001 | ||
|
|
77e4f69af0 | ||
|
|
8861909ea4 | ||
|
|
4b124e4c25 | ||
|
|
c45beeef6d | ||
|
|
a158397b6d | ||
|
|
a1fb6ae38f | ||
|
|
8c67ebc143 | ||
|
|
40d8bd43a1 | ||
|
|
c7ea1d07be | ||
|
|
e60de53404 | ||
|
|
3a1dc16c0d | ||
|
|
a6568fba7a | ||
|
|
0ab9e33110 | ||
|
|
8483850729 | ||
|
|
90608da5c2 | ||
|
|
6b8835b196 | ||
|
|
2bf36bb1db | ||
|
|
cc90cba78a | ||
|
|
a08bab7b18 | ||
|
|
f9c02889b2 | ||
|
|
9d4de2a722 | ||
|
|
92c5249746 | ||
|
|
b031ded671 | ||
|
|
5a295ad42b | ||
|
|
266477a4f5 | ||
|
|
07e3843918 | ||
|
|
c26c8affc7 | ||
|
|
629dd24ff3 | ||
|
|
c545d2e22e | ||
|
|
06383a4383 | ||
|
|
3a7083900c | ||
|
|
e2d3beca22 | ||
|
|
70dbbbd974 | ||
|
|
d60b7d46b7 | ||
|
|
d83f20f743 | ||
|
|
1c147b5c5f | ||
|
|
9c62149b00 | ||
|
|
027920ff52 | ||
|
|
a30921e67d | ||
|
|
7f9cc10447 | ||
|
|
b88cf64850 | ||
|
|
004179775c | ||
|
|
f95bd9c78f | ||
|
|
b97f0c0261 | ||
|
|
e886576a64 | ||
|
|
bb11b0f067 | ||
|
|
7a7c2ad416 | ||
|
|
fb680bc1e4 | ||
|
|
4f98818258 | ||
|
|
a5a896b519 | ||
|
|
0eb0faff03 | ||
|
|
4a23d4c7d3 | ||
|
|
377c61203d | ||
|
|
74a93fe764 | ||
|
|
ddbfdf14e9 | ||
|
|
e8ec74b944 | ||
|
|
f60f9bae00 | ||
|
|
eada1a184c | ||
|
|
34cfdb4e35 | ||
|
|
85e6f92c5a | ||
|
|
9efb90d23c | ||
|
|
66aa7d0e68 | ||
|
|
9f790325bb | ||
|
|
0fa7186296 | ||
|
|
90df932fe1 | ||
|
|
7436c0fe42 | ||
|
|
6766d25e62 | ||
|
|
9d9e11372b | ||
|
|
8ea0a8d40b | ||
|
|
56c7e78cf2 | ||
|
|
2fc0dfecb1 | ||
|
|
8c6b9b57cd | ||
|
|
f8438e96d1 | ||
|
|
2926989ec3 | ||
|
|
7d9e257713 | ||
|
|
031ee71adf | ||
|
|
d03dfd985b | ||
|
|
0e868deedd | ||
|
|
4984030871 | ||
|
|
7de509dc76 | ||
|
|
6f4657fe02 | ||
|
|
c0cd2d48ec | ||
|
|
d1eb5da5f4 | ||
|
|
c7492b0feb | ||
|
|
c20322232a | ||
|
|
61ca9bb8e4 | ||
|
|
1f8156e26c | ||
|
|
3e7b908a61 | ||
|
|
eb4a44535c | ||
|
|
ab8cf4f1e4 | ||
|
|
557720b094 | ||
|
|
92e19f6001 | ||
|
|
f4f42176bd | ||
|
|
59581786d3 | ||
|
|
3cf8610cb3 | ||
|
|
802497b05c | ||
|
|
df2f476c67 | ||
|
|
da338f2c1a | ||
|
|
fdea9cb426 | ||
|
|
fe2d24c240 | ||
|
|
faab0aa9df | ||
|
|
a521b885bf | ||
|
|
a744dc270b | ||
|
|
866c2ca994 | ||
|
|
bc8414a6f8 | ||
|
|
527f9cdfb2 | ||
|
|
4c04fe652c | ||
|
|
5e65e27bda | ||
|
|
7d3a962f73 | ||
|
|
ce998cdc87 | ||
|
|
dbbbfaa869 | ||
|
|
863edfd660 | ||
|
|
4d4967d0dd | ||
|
|
4b4f51fb6f | ||
|
|
30064655c2 | ||
|
|
fd5b92b2fb | ||
|
|
6e55c2a345 | ||
|
|
2134331e2b | ||
|
|
ffe83d9ab1 | ||
|
|
f2f649680f | ||
|
|
ece7b498ed | ||
|
|
65c2a25736 | ||
|
|
05586de51f | ||
|
|
a58b3aad59 | ||
|
|
e567e3d4e7 | ||
|
|
385f0298bd | ||
|
|
7edd241059 | ||
|
|
5bf6951311 | ||
|
|
24d0aa3f55 | ||
|
|
5ff7563070 | ||
|
|
def4e89372 | ||
|
|
8a62bc9237 | ||
|
|
471d94b6cd | ||
|
|
ce736e7ba1 | ||
|
|
455508deac | ||
|
|
0a3af545fe | ||
|
|
dd92318762 | ||
|
|
c931619269 | ||
|
|
1be440a72b | ||
|
|
04c7d5c128 | ||
|
|
30c77b9e64 | ||
|
|
21197fb968 | ||
|
|
b9ad19acbf |
65
.coveragerc
65
.coveragerc
@@ -73,7 +73,8 @@ omit =
|
||||
homeassistant/components/comfoconnect.py
|
||||
homeassistant/components/*/comfoconnect.py
|
||||
|
||||
homeassistant/components/daikin.py
|
||||
homeassistant/components/daikin/__init__.py
|
||||
homeassistant/components/daikin/const.py
|
||||
homeassistant/components/*/daikin.py
|
||||
|
||||
homeassistant/components/digital_ocean.py
|
||||
@@ -105,22 +106,33 @@ omit =
|
||||
homeassistant/components/enocean.py
|
||||
homeassistant/components/*/enocean.py
|
||||
|
||||
homeassistant/components/envisalink.py
|
||||
homeassistant/components/envisalink/__init__.py
|
||||
homeassistant/components/*/envisalink.py
|
||||
|
||||
homeassistant/components/evohome.py
|
||||
homeassistant/components/*/evohome.py
|
||||
|
||||
homeassistant/components/freebox.py
|
||||
homeassistant/components/*/freebox.py
|
||||
|
||||
homeassistant/components/fritzbox.py
|
||||
homeassistant/components/*/fritzbox.py
|
||||
|
||||
homeassistant/components/ecovacs.py
|
||||
homeassistant/components/*/ecovacs.py
|
||||
|
||||
homeassistant/components/esphome/__init__.py
|
||||
homeassistant/components/esphome/binary_sensor.py
|
||||
homeassistant/components/esphome/cover.py
|
||||
homeassistant/components/esphome/fan.py
|
||||
homeassistant/components/esphome/light.py
|
||||
homeassistant/components/esphome/sensor.py
|
||||
homeassistant/components/esphome/switch.py
|
||||
|
||||
homeassistant/components/eufy.py
|
||||
homeassistant/components/*/eufy.py
|
||||
|
||||
homeassistant/components/fibaro.py
|
||||
homeassistant/components/fibaro/__init__.py
|
||||
homeassistant/components/*/fibaro.py
|
||||
|
||||
homeassistant/components/gc100.py
|
||||
@@ -160,6 +172,9 @@ omit =
|
||||
homeassistant/components/homematicip_cloud.py
|
||||
homeassistant/components/*/homematicip_cloud.py
|
||||
|
||||
homeassistant/components/homeworks.py
|
||||
homeassistant/components/*/homeworks.py
|
||||
|
||||
homeassistant/components/huawei_lte.py
|
||||
homeassistant/components/*/huawei_lte.py
|
||||
|
||||
@@ -203,6 +218,9 @@ omit =
|
||||
homeassistant/components/lametric.py
|
||||
homeassistant/components/*/lametric.py
|
||||
|
||||
homeassistant/components/lcn.py
|
||||
homeassistant/components/*/lcn.py
|
||||
|
||||
homeassistant/components/linode.py
|
||||
homeassistant/components/*/linode.py
|
||||
|
||||
@@ -221,7 +239,7 @@ omit =
|
||||
homeassistant/components/lutron_caseta.py
|
||||
homeassistant/components/*/lutron_caseta.py
|
||||
|
||||
homeassistant/components/*/mailgun.py
|
||||
homeassistant/components/mailgun/notify.py
|
||||
|
||||
homeassistant/components/matrix.py
|
||||
homeassistant/components/*/matrix.py
|
||||
@@ -263,7 +281,11 @@ omit =
|
||||
homeassistant/components/*/opentherm_gw.py
|
||||
|
||||
homeassistant/components/openuv/__init__.py
|
||||
homeassistant/components/*/openuv.py
|
||||
homeassistant/components/openuv/binary_sensor.py
|
||||
homeassistant/components/openuv/sensor.py
|
||||
|
||||
homeassistant/components/plum_lightpad.py
|
||||
homeassistant/components/*/plum_lightpad.py
|
||||
|
||||
homeassistant/components/pilight.py
|
||||
homeassistant/components/*/pilight.py
|
||||
@@ -282,14 +304,21 @@ omit =
|
||||
homeassistant/components/*/raincloud.py
|
||||
|
||||
homeassistant/components/rainmachine/__init__.py
|
||||
homeassistant/components/*/rainmachine.py
|
||||
homeassistant/components/rainmachine/binary_sensor.py
|
||||
homeassistant/components/rainmachine/sensor.py
|
||||
homeassistant/components/rainmachine/switch.py
|
||||
|
||||
homeassistant/components/raspihats.py
|
||||
homeassistant/components/*/raspihats.py
|
||||
|
||||
homeassistant/components/*/raspyrfm.py
|
||||
|
||||
homeassistant/components/rfxtrx.py
|
||||
homeassistant/components/*/rfxtrx.py
|
||||
|
||||
homeassistant/components/roku.py
|
||||
homeassistant/components/*/roku.py
|
||||
|
||||
homeassistant/components/rpi_gpio.py
|
||||
homeassistant/components/*/rpi_gpio.py
|
||||
|
||||
@@ -309,7 +338,7 @@ omit =
|
||||
homeassistant/components/*/sense.py
|
||||
|
||||
homeassistant/components/simplisafe/__init__.py
|
||||
homeassistant/components/*/simplisafe.py
|
||||
homeassistant/components/simplisafe/alarm_control_panel.py
|
||||
|
||||
homeassistant/components/sisyphus.py
|
||||
homeassistant/components/*/sisyphus.py
|
||||
@@ -406,7 +435,14 @@ omit =
|
||||
homeassistant/components/*/zabbix.py
|
||||
|
||||
homeassistant/components/zha/__init__.py
|
||||
homeassistant/components/zha/binary_sensor.py
|
||||
homeassistant/components/zha/const.py
|
||||
homeassistant/components/zha/event.py
|
||||
homeassistant/components/zha/fan.py
|
||||
homeassistant/components/zha/light.py
|
||||
homeassistant/components/zha/sensor.py
|
||||
homeassistant/components/zha/switch.py
|
||||
homeassistant/components/zha/api.py
|
||||
homeassistant/components/zha/entities/*
|
||||
homeassistant/components/zha/helpers.py
|
||||
homeassistant/components/*/zha.py
|
||||
@@ -423,6 +459,7 @@ omit =
|
||||
homeassistant/components/spider.py
|
||||
homeassistant/components/*/spider.py
|
||||
|
||||
homeassistant/components/air_quality/opensensemap.py
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/canary.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
@@ -496,11 +533,9 @@ omit =
|
||||
homeassistant/components/device_tracker/bt_smarthub.py
|
||||
homeassistant/components/device_tracker/cisco_ios.py
|
||||
homeassistant/components/device_tracker/ddwrt.py
|
||||
homeassistant/components/device_tracker/freebox.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/google_maps.py
|
||||
homeassistant/components/device_tracker/googlehome.py
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
homeassistant/components/device_tracker/hitron_coda.py
|
||||
homeassistant/components/device_tracker/huawei_router.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
@@ -533,6 +568,7 @@ omit =
|
||||
homeassistant/components/folder_watcher.py
|
||||
homeassistant/components/foursquare.py
|
||||
homeassistant/components/goalfeed.py
|
||||
homeassistant/components/idteck_prox.py
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/image_processing/dlib_face_detect.py
|
||||
homeassistant/components/image_processing/dlib_face_identify.py
|
||||
@@ -596,6 +632,7 @@ omit =
|
||||
homeassistant/components/media_player/frontier_silicon.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
homeassistant/components/media_player/gstreamer.py
|
||||
homeassistant/components/media_player/harman_kardon_avr.py
|
||||
homeassistant/components/media_player/horizon.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
@@ -616,7 +653,6 @@ omit =
|
||||
homeassistant/components/media_player/pioneer.py
|
||||
homeassistant/components/media_player/pjlink.py
|
||||
homeassistant/components/media_player/plex.py
|
||||
homeassistant/components/media_player/roku.py
|
||||
homeassistant/components/media_player/russound_rio.py
|
||||
homeassistant/components/media_player/russound_rnet.py
|
||||
homeassistant/components/media_player/snapcast.py
|
||||
@@ -680,8 +716,10 @@ omit =
|
||||
homeassistant/components/route53.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/scene/lifx_cloud.py
|
||||
homeassistant/components/sensor/aftership.py
|
||||
homeassistant/components/sensor/airvisual.py
|
||||
homeassistant/components/sensor/alpha_vantage.py
|
||||
homeassistant/components/sensor/ambient_station.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
@@ -692,6 +730,7 @@ omit =
|
||||
homeassistant/components/sensor/bme680.py
|
||||
homeassistant/components/sensor/bom.py
|
||||
homeassistant/components/sensor/broadlink.py
|
||||
homeassistant/components/sensor/brottsplatskartan.py
|
||||
homeassistant/components/sensor/buienradar.py
|
||||
homeassistant/components/sensor/cert_expiry.py
|
||||
homeassistant/components/sensor/citybikes.py
|
||||
@@ -737,6 +776,7 @@ omit =
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
homeassistant/components/sensor/gpsd.py
|
||||
homeassistant/components/sensor/gtfs.py
|
||||
homeassistant/components/sensor/gtt.py
|
||||
homeassistant/components/sensor/haveibeenpwned.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/htu21d.py
|
||||
@@ -752,6 +792,7 @@ omit =
|
||||
homeassistant/components/sensor/launch_library.py
|
||||
homeassistant/components/sensor/linky.py
|
||||
homeassistant/components/sensor/linux_battery.py
|
||||
homeassistant/components/sensor/london_underground.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/luftdaten.py
|
||||
homeassistant/components/sensor/lyft.py
|
||||
@@ -769,6 +810,7 @@ omit =
|
||||
homeassistant/components/sensor/netdata.py
|
||||
homeassistant/components/sensor/netdata_public.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/nmbs.py
|
||||
homeassistant/components/sensor/noaa_tides.py
|
||||
homeassistant/components/sensor/nsw_fuel_station.py
|
||||
homeassistant/components/sensor/nut.py
|
||||
@@ -785,6 +827,7 @@ omit =
|
||||
homeassistant/components/sensor/pocketcasts.py
|
||||
homeassistant/components/sensor/pollen.py
|
||||
homeassistant/components/sensor/postnl.py
|
||||
homeassistant/components/sensor/prezzibenzina.py
|
||||
homeassistant/components/sensor/pushbullet.py
|
||||
homeassistant/components/sensor/pvoutput.py
|
||||
homeassistant/components/sensor/pyload.py
|
||||
@@ -809,6 +852,7 @@ omit =
|
||||
homeassistant/components/sensor/snmp.py
|
||||
homeassistant/components/sensor/sochain.py
|
||||
homeassistant/components/sensor/socialblade.py
|
||||
homeassistant/components/sensor/solaredge.py
|
||||
homeassistant/components/sensor/sonarr.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
homeassistant/components/sensor/spotcrime.py
|
||||
@@ -864,6 +908,7 @@ omit =
|
||||
homeassistant/components/switch/mystrom.py
|
||||
homeassistant/components/switch/netio.py
|
||||
homeassistant/components/switch/orvibo.py
|
||||
homeassistant/components/switch/pencom.py
|
||||
homeassistant/components/switch/pulseaudio_loopback.py
|
||||
homeassistant/components/switch/rainbird.py
|
||||
homeassistant/components/switch/rest.py
|
||||
|
||||
1
.github/ISSUE_TEMPLATE.md
vendored
1
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,6 +1,7 @@
|
||||
<!-- READ THIS FIRST:
|
||||
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||
- Frontend issues should be submitted to the home-assistant-polymer repository: https://github.com/home-assistant/home-assistant-polymer/issues
|
||||
- Do not report issues for components if you are using custom components: files in <config-dir>/custom_components
|
||||
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
||||
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -7,6 +7,7 @@ about: Create a report to help us improve
|
||||
<!-- READ THIS FIRST:
|
||||
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||
- Frontend issues should be submitted to the home-assistant-polymer repository: https://github.com/home-assistant/home-assistant-polymer/issues
|
||||
- Do not report issues for components if you are using custom components: files in <config-dir>/custom_components
|
||||
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
||||
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -78,6 +78,7 @@ venv
|
||||
.venv
|
||||
Pipfile*
|
||||
share/*
|
||||
Scripts/
|
||||
|
||||
# vimmy stuff
|
||||
*.swp
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[settings]
|
||||
multi_line_output=4
|
||||
@@ -184,6 +184,7 @@ homeassistant/components/*/edp_redy.py @abmantis
|
||||
homeassistant/components/edp_redy.py @abmantis
|
||||
homeassistant/components/eight_sleep.py @mezz64
|
||||
homeassistant/components/*/eight_sleep.py @mezz64
|
||||
homeassistant/components/esphome/*.py @OttoWinter
|
||||
|
||||
# H
|
||||
homeassistant/components/hive.py @Rendili @KJonline
|
||||
@@ -211,9 +212,12 @@ homeassistant/components/melissa.py @kennedyshead
|
||||
homeassistant/components/*/melissa.py @kennedyshead
|
||||
homeassistant/components/*/mystrom.py @fabaff
|
||||
|
||||
# N
|
||||
homeassistant/components/ness_alarm.py @nickw444
|
||||
homeassistant/components/*/ness_alarm.py @nickw444
|
||||
|
||||
# O
|
||||
homeassistant/components/openuv/* @bachya
|
||||
homeassistant/components/*/openuv.py @bachya
|
||||
|
||||
# P
|
||||
homeassistant/components/point/* @fredrike
|
||||
@@ -225,13 +229,11 @@ homeassistant/components/*/qwikswitch.py @kellerza
|
||||
|
||||
# R
|
||||
homeassistant/components/rainmachine/* @bachya
|
||||
homeassistant/components/*/rainmachine.py @bachya
|
||||
homeassistant/components/*/random.py @fabaff
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
|
||||
# S
|
||||
homeassistant/components/simplisafe/* @bachya
|
||||
homeassistant/components/*/simplisafe.py @bachya
|
||||
|
||||
# T
|
||||
homeassistant/components/tahoma.py @philklei
|
||||
|
||||
@@ -16,7 +16,6 @@ LABEL maintainer="Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
|
||||
|
||||
VOLUME /config
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Copy build scripts
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Home Assistant |Build Status| |Coverage Status| |Chat Status| |Reviewed by Hound|
|
||||
Home Assistant |Build Status| |Coverage Status| |Chat Status|
|
||||
=================================================================================
|
||||
|
||||
Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control.
|
||||
@@ -33,8 +33,6 @@ of a component, check the `Home Assistant help section <https://home-assistant.i
|
||||
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
|
||||
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
|
||||
:target: https://discord.gg/c5DvZ4e
|
||||
.. |Reviewed by Hound| image:: https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg
|
||||
:target: https://houndci.com
|
||||
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
|
||||
:target: https://home-assistant.io/demo/
|
||||
.. |screenshot-components| image:: https://raw.github.com/home-assistant/home-assistant/dev/docs/screenshot-components.png
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Permission constants."""
|
||||
CAT_ENTITIES = 'entities'
|
||||
CAT_CONFIG_ENTRIES = 'config_entries'
|
||||
SUBCAT_ALL = 'all'
|
||||
|
||||
POLICY_READ = 'read'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Home Assistant auth provider."""
|
||||
import base64
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, cast
|
||||
|
||||
import bcrypt
|
||||
@@ -51,6 +52,15 @@ class Data:
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
|
||||
private=True)
|
||||
self._data = None # type: Optional[Dict[str, Any]]
|
||||
self.is_legacy = False
|
||||
|
||||
@callback
|
||||
def normalize_username(self, username: str) -> str:
|
||||
"""Normalize a username based on the mode."""
|
||||
if self.is_legacy:
|
||||
return username
|
||||
|
||||
return username.strip()
|
||||
|
||||
async def async_load(self) -> None:
|
||||
"""Load stored data."""
|
||||
@@ -61,6 +71,20 @@ class Data:
|
||||
'users': []
|
||||
}
|
||||
|
||||
for user in data['users']:
|
||||
username = user['username']
|
||||
|
||||
# check if we have unstripped usernames
|
||||
if username != username.strip():
|
||||
self.is_legacy = True
|
||||
|
||||
logging.getLogger(__name__).warning(
|
||||
"Home Assistant auth provider is running in legacy mode "
|
||||
"because we detected usernames that start or end in a "
|
||||
"space. Please change the username.")
|
||||
|
||||
break
|
||||
|
||||
self._data = data
|
||||
|
||||
@property
|
||||
@@ -73,6 +97,7 @@ class Data:
|
||||
|
||||
Raises InvalidAuth if auth invalid.
|
||||
"""
|
||||
username = self.normalize_username(username)
|
||||
dummy = b'$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO'
|
||||
found = None
|
||||
|
||||
@@ -105,7 +130,10 @@ class Data:
|
||||
|
||||
def add_auth(self, username: str, password: str) -> None:
|
||||
"""Add a new authenticated user/pass."""
|
||||
if any(user['username'] == username for user in self.users):
|
||||
username = self.normalize_username(username)
|
||||
|
||||
if any(self.normalize_username(user['username']) == username
|
||||
for user in self.users):
|
||||
raise InvalidUser
|
||||
|
||||
self.users.append({
|
||||
@@ -116,9 +144,11 @@ class Data:
|
||||
@callback
|
||||
def async_remove_auth(self, username: str) -> None:
|
||||
"""Remove authentication."""
|
||||
username = self.normalize_username(username)
|
||||
|
||||
index = None
|
||||
for i, user in enumerate(self.users):
|
||||
if user['username'] == username:
|
||||
if self.normalize_username(user['username']) == username:
|
||||
index = i
|
||||
break
|
||||
|
||||
@@ -132,8 +162,10 @@ class Data:
|
||||
|
||||
Raises InvalidUser if user cannot be found.
|
||||
"""
|
||||
username = self.normalize_username(username)
|
||||
|
||||
for user in self.users:
|
||||
if user['username'] == username:
|
||||
if self.normalize_username(user['username']) == username:
|
||||
user['password'] = self.hash_password(
|
||||
new_password, True).decode()
|
||||
break
|
||||
@@ -178,10 +210,15 @@ class HassAuthProvider(AuthProvider):
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]) -> Credentials:
|
||||
"""Get credentials based on the flow result."""
|
||||
username = flow_result['username']
|
||||
if self.data is None:
|
||||
await self.async_initialize()
|
||||
assert self.data is not None
|
||||
|
||||
norm_username = self.data.normalize_username
|
||||
username = norm_username(flow_result['username'])
|
||||
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data['username'] == username:
|
||||
if norm_username(credential.data['username']) == username:
|
||||
return credential
|
||||
|
||||
# Create new credentials.
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.util.logging import AsyncHandler
|
||||
from homeassistant.util.package import async_get_user_site, is_virtual_env
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -153,6 +154,34 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
stop = time()
|
||||
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
|
||||
|
||||
# TEMP: warn users for invalid slugs
|
||||
# Remove after 0.94 or 1.0
|
||||
if cv.INVALID_SLUGS_FOUND or cv.INVALID_ENTITY_IDS_FOUND:
|
||||
msg = []
|
||||
|
||||
if cv.INVALID_ENTITY_IDS_FOUND:
|
||||
msg.append(
|
||||
"Your configuration contains invalid entity ID references. "
|
||||
"Please find and update the following. "
|
||||
"This will become a breaking change."
|
||||
)
|
||||
msg.append('\n'.join('- {} -> {}'.format(*item)
|
||||
for item
|
||||
in cv.INVALID_ENTITY_IDS_FOUND.items()))
|
||||
|
||||
if cv.INVALID_SLUGS_FOUND:
|
||||
msg.append(
|
||||
"Your configuration contains invalid slugs. "
|
||||
"Please find and update the following. "
|
||||
"This will become a breaking change."
|
||||
)
|
||||
msg.append('\n'.join('- {} -> {}'.format(*item)
|
||||
for item in cv.INVALID_SLUGS_FOUND.items()))
|
||||
|
||||
hass.components.persistent_notification.async_create(
|
||||
'\n\n'.join(msg), "Config Warning", "config_warning"
|
||||
)
|
||||
|
||||
return hass
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['abodepy==0.14.0']
|
||||
REQUIREMENTS = ['abodepy==0.15.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
|
||||
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({
|
||||
vol.Required(CONF_ADS_TYPE):
|
||||
vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE]),
|
||||
vol.Required(CONF_ADS_VALUE): cv.match_all,
|
||||
vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE, ADSTYPE_BOOL]),
|
||||
vol.Required(CONF_ADS_VALUE): vol.Coerce(int),
|
||||
vol.Required(CONF_ADS_VAR): cv.string,
|
||||
})
|
||||
|
||||
@@ -125,16 +125,23 @@ class AdsHub:
|
||||
|
||||
def shutdown(self, *args, **kwargs):
|
||||
"""Shutdown ADS connection."""
|
||||
import pyads
|
||||
_LOGGER.debug("Shutting down ADS")
|
||||
for notification_item in self._notification_items.values():
|
||||
self._client.del_device_notification(
|
||||
notification_item.hnotify,
|
||||
notification_item.huser
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Deleting device notification %d, %d",
|
||||
notification_item.hnotify, notification_item.huser)
|
||||
self._client.close()
|
||||
try:
|
||||
self._client.del_device_notification(
|
||||
notification_item.hnotify,
|
||||
notification_item.huser
|
||||
)
|
||||
except pyads.ADSError as err:
|
||||
_LOGGER.error(err)
|
||||
try:
|
||||
self._client.close()
|
||||
except pyads.ADSError as err:
|
||||
_LOGGER.error(err)
|
||||
|
||||
def register_device(self, device):
|
||||
"""Register a new device."""
|
||||
|
||||
147
homeassistant/components/air_quality/__init__.py
Normal file
147
homeassistant/components/air_quality/__init__.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
Component for handling Air Quality data for your location.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/air_quality/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_AQI = 'air_quality_index'
|
||||
ATTR_ATTRIBUTION = 'attribution'
|
||||
ATTR_C02 = 'carbon_dioxide'
|
||||
ATTR_CO = 'carbon_monoxide'
|
||||
ATTR_N2O = 'nitrogen_oxide'
|
||||
ATTR_NO = 'nitrogen_monoxide'
|
||||
ATTR_NO2 = 'nitrogen_dioxide'
|
||||
ATTR_OZONE = 'ozone'
|
||||
ATTR_PM_0_1 = 'particulate_matter_0_1'
|
||||
ATTR_PM_10 = 'particulate_matter_10'
|
||||
ATTR_PM_2_5 = 'particulate_matter_2_5'
|
||||
ATTR_SO2 = 'sulphur_dioxide'
|
||||
|
||||
DOMAIN = 'air_quality'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
PROP_TO_ATTR = {
|
||||
'air_quality_index': ATTR_AQI,
|
||||
'attribution': ATTR_ATTRIBUTION,
|
||||
'carbon_dioxide': ATTR_C02,
|
||||
'carbon_monoxide': ATTR_CO,
|
||||
'nitrogen_oxide': ATTR_N2O,
|
||||
'nitrogen_monoxide': ATTR_NO,
|
||||
'nitrogen_dioxide': ATTR_NO2,
|
||||
'ozone': ATTR_OZONE,
|
||||
'particulate_matter_0_1': ATTR_PM_0_1,
|
||||
'particulate_matter_10': ATTR_PM_10,
|
||||
'particulate_matter_2_5': ATTR_PM_2_5,
|
||||
'sulphur_dioxide': ATTR_SO2,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the air quality component."""
|
||||
component = hass.data[DOMAIN] = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
await component.async_setup(config)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up a config entry."""
|
||||
return await hass.data[DOMAIN].async_setup_entry(entry)
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload a config entry."""
|
||||
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||
|
||||
|
||||
class AirQualityEntity(Entity):
|
||||
"""ABC for air quality data."""
|
||||
|
||||
@property
|
||||
def particulate_matter_2_5(self):
|
||||
"""Return the particulate matter 2.5 level."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def particulate_matter_10(self):
|
||||
"""Return the particulate matter 10 level."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def particulate_matter_0_1(self):
|
||||
"""Return the particulate matter 0.1 level."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def air_quality_index(self):
|
||||
"""Return the Air Quality Index (AQI)."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def ozone(self):
|
||||
"""Return the O3 (ozone) level."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def carbon_monoxide(self):
|
||||
"""Return the CO (carbon monoxide) level."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def carbon_dioxide(self):
|
||||
"""Return the CO2 (carbon dioxide) level."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
"""Return the attribution."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def sulphur_dioxide(self):
|
||||
"""Return the SO2 (sulphur dioxide) level."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def nitrogen_oxide(self):
|
||||
"""Return the N2O (nitrogen oxide) level."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def nitrogen_monoxide(self):
|
||||
"""Return the NO (nitrogen monoxide) level."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def nitrogen_dioxide(self):
|
||||
"""Return the NO2 (nitrogen dioxide) level."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
data = {}
|
||||
|
||||
for prop, attr in PROP_TO_ATTR.items():
|
||||
value = getattr(self, prop)
|
||||
if value is not None:
|
||||
data[attr] = value
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state."""
|
||||
return self.particulate_matter_2_5
|
||||
56
homeassistant/components/air_quality/demo.py
Normal file
56
homeassistant/components/air_quality/demo.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
Demo platform that offers fake air quality data.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
from homeassistant.components.air_quality import AirQualityEntity
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Air Quality."""
|
||||
add_entities([
|
||||
DemoAirQuality('Home', 14, 23, 100),
|
||||
DemoAirQuality('Office', 4, 16, None)
|
||||
])
|
||||
|
||||
|
||||
class DemoAirQuality(AirQualityEntity):
|
||||
"""Representation of Air Quality data."""
|
||||
|
||||
def __init__(self, name, pm_2_5, pm_10, n2o):
|
||||
"""Initialize the Demo Air Quality."""
|
||||
self._name = name
|
||||
self._pm_2_5 = pm_2_5
|
||||
self._pm_10 = pm_10
|
||||
self._n2o = n2o
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return '{} {}'.format('Demo Air Quality', self._name)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed for Demo Air Quality."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def particulate_matter_2_5(self):
|
||||
"""Return the particulate matter 2.5 level."""
|
||||
return self._pm_2_5
|
||||
|
||||
@property
|
||||
def particulate_matter_10(self):
|
||||
"""Return the particulate matter 10 level."""
|
||||
return self._pm_10
|
||||
|
||||
@property
|
||||
def nitrogen_oxide(self):
|
||||
"""Return the nitrogen oxide (N2O) level."""
|
||||
return self._n2o
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
"""Return the attribution."""
|
||||
return 'Powered by Home Assistant'
|
||||
105
homeassistant/components/air_quality/opensensemap.py
Normal file
105
homeassistant/components/air_quality/opensensemap.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
Support for openSenseMap Air Quality data.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/air_quality/opensensemap/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.air_quality import (
|
||||
PLATFORM_SCHEMA, AirQualityEntity)
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['opensensemap-api==0.1.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTRIBUTION = 'Data provided by openSenseMap'
|
||||
|
||||
CONF_STATION_ID = 'station_id'
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_STATION_ID): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the openSenseMap air quality platform."""
|
||||
from opensensemap_api import OpenSenseMap
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
station_id = config[CONF_STATION_ID]
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
osm_api = OpenSenseMapData(OpenSenseMap(station_id, hass.loop, session))
|
||||
|
||||
await osm_api.async_update()
|
||||
|
||||
if 'name' not in osm_api.api.data:
|
||||
_LOGGER.error("Station %s is not available", station_id)
|
||||
return
|
||||
|
||||
station_name = osm_api.api.data['name'] if name is None else name
|
||||
|
||||
async_add_entities([OpenSenseMapQuality(station_name, osm_api)], True)
|
||||
|
||||
|
||||
class OpenSenseMapQuality(AirQualityEntity):
|
||||
"""Implementation of an openSenseMap air quality entity."""
|
||||
|
||||
def __init__(self, name, osm):
|
||||
"""Initialize the air quality entity."""
|
||||
self._name = name
|
||||
self._osm = osm
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the air quality entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def particulate_matter_2_5(self):
|
||||
"""Return the particulate matter 2.5 level."""
|
||||
return self._osm.api.pm2_5
|
||||
|
||||
@property
|
||||
def particulate_matter_10(self):
|
||||
"""Return the particulate matter 10 level."""
|
||||
return self._osm.api.pm10
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
"""Return the attribution."""
|
||||
return ATTRIBUTION
|
||||
|
||||
async def async_update(self):
|
||||
"""Get the latest data from the openSenseMap API."""
|
||||
await self._osm.async_update()
|
||||
|
||||
|
||||
class OpenSenseMapData:
|
||||
"""Get the latest data and update the states."""
|
||||
|
||||
def __init__(self, api):
|
||||
"""Initialize the data object."""
|
||||
self.api = api
|
||||
|
||||
@Throttle(SCAN_INTERVAL)
|
||||
async def async_update(self):
|
||||
"""Get the latest data from the Pi-hole."""
|
||||
from opensensemap_api.exceptions import OpenSenseMapError
|
||||
|
||||
try:
|
||||
await self.api.get_data()
|
||||
except OpenSenseMapError as err:
|
||||
_LOGGER.error("Unable to fetch data: %s", err)
|
||||
@@ -21,11 +21,13 @@ from homeassistant.helpers.entity_component import EntityComponent
|
||||
DOMAIN = 'alarm_control_panel'
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
ATTR_CHANGED_BY = 'changed_by'
|
||||
FORMAT_TEXT = 'text'
|
||||
FORMAT_NUMBER = 'number'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
ALARM_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
vol.Optional(ATTR_CODE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more digits/characters."""
|
||||
return 'Number'
|
||||
return alarm.FORMAT_NUMBER
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -81,8 +81,8 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
if self._code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
return alarm.FORMAT_NUMBER
|
||||
return alarm.FORMAT_TEXT
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.components.arlo import (
|
||||
DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO)
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED)
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,6 +25,7 @@ ARMED = 'armed'
|
||||
|
||||
CONF_HOME_MODE_NAME = 'home_mode_name'
|
||||
CONF_AWAY_MODE_NAME = 'away_mode_name'
|
||||
CONF_NIGHT_MODE_NAME = 'night_mode_name'
|
||||
|
||||
DEPENDENCIES = ['arlo']
|
||||
|
||||
@@ -35,6 +36,7 @@ ICON = 'mdi:security'
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string,
|
||||
vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string,
|
||||
vol.Optional(CONF_NIGHT_MODE_NAME, default=ARMED): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -47,21 +49,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
home_mode_name = config.get(CONF_HOME_MODE_NAME)
|
||||
away_mode_name = config.get(CONF_AWAY_MODE_NAME)
|
||||
night_mode_name = config.get(CONF_NIGHT_MODE_NAME)
|
||||
base_stations = []
|
||||
for base_station in arlo.base_stations:
|
||||
base_stations.append(ArloBaseStation(base_station, home_mode_name,
|
||||
away_mode_name))
|
||||
away_mode_name, night_mode_name))
|
||||
add_entities(base_stations, True)
|
||||
|
||||
|
||||
class ArloBaseStation(AlarmControlPanel):
|
||||
"""Representation of an Arlo Alarm Control Panel."""
|
||||
|
||||
def __init__(self, data, home_mode_name, away_mode_name):
|
||||
def __init__(self, data, home_mode_name, away_mode_name, night_mode_name):
|
||||
"""Initialize the alarm control panel."""
|
||||
self._base_station = data
|
||||
self._home_mode_name = home_mode_name
|
||||
self._away_mode_name = away_mode_name
|
||||
self._night_mode_name = night_mode_name
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
@@ -105,6 +109,10 @@ class ArloBaseStation(AlarmControlPanel):
|
||||
"""Send arm home command. Uses custom mode."""
|
||||
self._base_station.mode = self._home_mode_name
|
||||
|
||||
async def async_alarm_arm_night(self, code=None):
|
||||
"""Send arm night command. Uses custom mode."""
|
||||
self._base_station.mode = self._night_mode_name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the base station."""
|
||||
@@ -128,4 +136,6 @@ class ArloBaseStation(AlarmControlPanel):
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
if mode == self._away_mode_name:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
if mode == self._night_mode_name:
|
||||
return STATE_ALARM_ARMED_NIGHT
|
||||
return mode
|
||||
|
||||
@@ -80,7 +80,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the characters if code is defined."""
|
||||
return 'Number'
|
||||
return alarm.FORMAT_NUMBER
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -116,7 +116,7 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel):
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the alarm code format."""
|
||||
return '^[0-9]{4}([0-9]{2})?$'
|
||||
return alarm.FORMAT_NUMBER
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -104,7 +104,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
"""Regex for code format or None if no code is required."""
|
||||
if self._code:
|
||||
return None
|
||||
return 'Number'
|
||||
return alarm.FORMAT_NUMBER
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
Support for Homekit Alarm Control Panel.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.homekit_controller/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.homekit_controller import (HomeKitEntity,
|
||||
KNOWN_ACCESSORIES)
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED)
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
|
||||
DEPENDENCIES = ['homekit_controller']
|
||||
|
||||
ICON = 'mdi:security'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CURRENT_STATE_MAP = {
|
||||
0: STATE_ALARM_ARMED_HOME,
|
||||
1: STATE_ALARM_ARMED_AWAY,
|
||||
2: STATE_ALARM_ARMED_NIGHT,
|
||||
3: STATE_ALARM_DISARMED,
|
||||
4: STATE_ALARM_TRIGGERED
|
||||
}
|
||||
|
||||
TARGET_STATE_MAP = {
|
||||
STATE_ALARM_ARMED_HOME: 0,
|
||||
STATE_ALARM_ARMED_AWAY: 1,
|
||||
STATE_ALARM_ARMED_NIGHT: 2,
|
||||
STATE_ALARM_DISARMED: 3,
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up Homekit Alarm Control Panel support."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']]
|
||||
add_entities([HomeKitAlarmControlPanel(accessory, discovery_info)],
|
||||
True)
|
||||
|
||||
|
||||
class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel):
|
||||
"""Representation of a Homekit Alarm Control Panel."""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""Initialise the Alarm Control Panel."""
|
||||
super().__init__(*args)
|
||||
self._state = None
|
||||
self._battery_level = None
|
||||
|
||||
def update_characteristics(self, characteristics):
|
||||
"""Synchronise the Alarm Control Panel state with Home Assistant."""
|
||||
# pylint: disable=import-error
|
||||
from homekit.model.characteristics import CharacteristicsTypes
|
||||
|
||||
for characteristic in characteristics:
|
||||
ctype = characteristic['type']
|
||||
ctype = CharacteristicsTypes.get_short(ctype)
|
||||
if ctype == "security-system-state.current":
|
||||
self._chars['security-system-state.current'] = \
|
||||
characteristic['iid']
|
||||
self._state = CURRENT_STATE_MAP[characteristic['value']]
|
||||
elif ctype == "security-system-state.target":
|
||||
self._chars['security-system-state.target'] = \
|
||||
characteristic['iid']
|
||||
elif ctype == "battery-level":
|
||||
self._chars['battery-level'] = characteristic['iid']
|
||||
self._battery_level = characteristic['value']
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return icon."""
|
||||
return ICON
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self.set_alarm_state(STATE_ALARM_DISARMED, code)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm command."""
|
||||
self.set_alarm_state(STATE_ALARM_ARMED_AWAY, code)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send stay command."""
|
||||
self.set_alarm_state(STATE_ALARM_ARMED_HOME, code)
|
||||
|
||||
def alarm_arm_night(self, code=None):
|
||||
"""Send night command."""
|
||||
self.set_alarm_state(STATE_ALARM_ARMED_NIGHT, code)
|
||||
|
||||
def set_alarm_state(self, state, code=None):
|
||||
"""Send state command."""
|
||||
characteristics = [{'aid': self._aid,
|
||||
'iid': self._chars['security-system-state.target'],
|
||||
'value': TARGET_STATE_MAP[state]}]
|
||||
self.put_characteristics(characteristics)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
if self._battery_level is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
ATTR_BATTERY_LEVEL: self._battery_level,
|
||||
}
|
||||
@@ -5,14 +5,16 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.ialarm/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
|
||||
CONF_CODE, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyialarm==0.3']
|
||||
@@ -36,6 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): vol.All(cv.string, no_application_protocol),
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.positive_int,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
@@ -43,23 +46,25 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up an iAlarm control panel."""
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
host = config.get(CONF_HOST)
|
||||
|
||||
url = 'http://{}'.format(host)
|
||||
ialarm = IAlarmPanel(name, username, password, url)
|
||||
ialarm = IAlarmPanel(name, code, username, password, url)
|
||||
add_entities([ialarm], True)
|
||||
|
||||
|
||||
class IAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Representation of an iAlarm status."""
|
||||
|
||||
def __init__(self, name, username, password, url):
|
||||
def __init__(self, name, code, username, password, url):
|
||||
"""Initialize the iAlarm status."""
|
||||
from pyialarm import IAlarm
|
||||
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._url = url
|
||||
@@ -71,6 +76,15 @@ class IAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return alarm.FORMAT_NUMBER
|
||||
return alarm.FORMAT_TEXT
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
@@ -98,12 +112,22 @@ class IAlarmPanel(alarm.AlarmControlPanel):
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._client.disarm()
|
||||
if self._validate_code(code):
|
||||
self._client.disarm()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._client.arm_away()
|
||||
if self._validate_code(code):
|
||||
self._client.arm_away()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._client.arm_stay()
|
||||
if self._validate_code(code):
|
||||
self._client.arm_stay()
|
||||
|
||||
def _validate_code(self, code):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if not check:
|
||||
_LOGGER.warning("Wrong code entered")
|
||||
return check
|
||||
|
||||
@@ -129,8 +129,8 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel):
|
||||
if self._code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
return alarm.FORMAT_NUMBER
|
||||
return alarm.FORMAT_TEXT
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -207,8 +207,8 @@ class ManualAlarm(alarm.AlarmControlPanel, RestoreEntity):
|
||||
if self._code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
return alarm.FORMAT_NUMBER
|
||||
return alarm.FORMAT_TEXT
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -241,8 +241,8 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
if self._code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
return alarm.FORMAT_NUMBER
|
||||
return alarm.FORMAT_TEXT
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
107
homeassistant/components/alarm_control_panel/ness_alarm.py
Normal file
107
homeassistant/components/alarm_control_panel/ness_alarm.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
Support for Ness D8X/D16X alarm panel.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.ness_alarm/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.ness_alarm import (
|
||||
DATA_NESS, SIGNAL_ARMING_STATE_CHANGED)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMING,
|
||||
STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, STATE_ALARM_DISARMED)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['ness_alarm']
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the Ness Alarm alarm control panel devices."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
device = NessAlarmPanel(hass.data[DATA_NESS], 'Alarm Panel')
|
||||
async_add_entities([device])
|
||||
|
||||
|
||||
class NessAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Representation of a Ness alarm panel."""
|
||||
|
||||
def __init__(self, client, name):
|
||||
"""Initialize the alarm panel."""
|
||||
self._client = client
|
||||
self._name = name
|
||||
self._state = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_ARMING_STATE_CHANGED,
|
||||
self._handle_arming_state_change)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the regex for code format or None if no code is required."""
|
||||
return alarm.FORMAT_NUMBER
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
await self._client.disarm(code)
|
||||
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
await self._client.arm_away(code)
|
||||
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
await self._client.arm_home(code)
|
||||
|
||||
async def async_alarm_trigger(self, code=None):
|
||||
"""Send trigger/panic command."""
|
||||
await self._client.panic(code)
|
||||
|
||||
@callback
|
||||
def _handle_arming_state_change(self, arming_state):
|
||||
"""Handle arming state update."""
|
||||
from nessclient import ArmingState
|
||||
|
||||
if arming_state == ArmingState.UNKNOWN:
|
||||
self._state = None
|
||||
elif arming_state == ArmingState.DISARMED:
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif arming_state == ArmingState.ARMING:
|
||||
self._state = STATE_ALARM_ARMING
|
||||
elif arming_state == ArmingState.EXIT_DELAY:
|
||||
self._state = STATE_ALARM_ARMING
|
||||
elif arming_state == ArmingState.ARMED:
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
elif arming_state == ArmingState.ENTRY_DELAY:
|
||||
self._state = STATE_ALARM_PENDING
|
||||
elif arming_state == ArmingState.TRIGGERED:
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
else:
|
||||
_LOGGER.warning("Unhandled arming state: %s", arming_state)
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
@@ -13,7 +13,7 @@ import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pynx584==0.4']
|
||||
@@ -43,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
add_entities([NX584Alarm(hass, url, name)])
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to NX584: %s", str(ex))
|
||||
return False
|
||||
return
|
||||
|
||||
|
||||
class NX584Alarm(alarm.AlarmControlPanel):
|
||||
@@ -60,7 +60,7 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
# talk to the API and trigger a requests exception for setup_platform()
|
||||
# to catch
|
||||
self._alarm.list_zones()
|
||||
self._state = STATE_UNKNOWN
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -70,7 +70,7 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more digits/characters."""
|
||||
return 'Number'
|
||||
return alarm.FORMAT_NUMBER
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@@ -85,11 +85,11 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to %(host)s: %(reason)s",
|
||||
dict(host=self._url, reason=ex))
|
||||
self._state = STATE_UNKNOWN
|
||||
self._state = None
|
||||
zones = []
|
||||
except IndexError:
|
||||
_LOGGER.error("NX584 reports no partitions")
|
||||
self._state = STATE_UNKNOWN
|
||||
self._state = None
|
||||
zones = []
|
||||
|
||||
bypassed = False
|
||||
@@ -107,6 +107,10 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
else:
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
|
||||
for flag in part['condition_flags']:
|
||||
if flag == "Siren on":
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._alarm.disarm(code)
|
||||
|
||||
@@ -64,7 +64,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the regex for code format or None if no code is required."""
|
||||
return 'Number'
|
||||
return alarm.FORMAT_NUMBER
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -61,7 +61,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more digits/characters."""
|
||||
return 'Number'
|
||||
return alarm.FORMAT_NUMBER
|
||||
|
||||
@property
|
||||
def changed_by(self):
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['yalesmartalarmclient==0.1.5']
|
||||
REQUIREMENTS = ['yalesmartalarmclient==0.1.6']
|
||||
|
||||
CONF_AREA_ID = 'area_id'
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ CONF_DEVICE_TYPE = 'type'
|
||||
CONF_PANEL_DISPLAY = 'panel_display'
|
||||
CONF_ZONE_NAME = 'name'
|
||||
CONF_ZONE_TYPE = 'type'
|
||||
CONF_ZONE_LOOP = 'loop'
|
||||
CONF_ZONE_RFID = 'rfid'
|
||||
CONF_ZONES = 'zones'
|
||||
CONF_RELAY_ADDR = 'relayaddr'
|
||||
@@ -75,6 +76,8 @@ ZONE_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_ZONE_TYPE,
|
||||
default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA),
|
||||
vol.Optional(CONF_ZONE_RFID): cv.string,
|
||||
vol.Optional(CONF_ZONE_LOOP):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=4)),
|
||||
vol.Inclusive(CONF_RELAY_ADDR, 'relaylocation',
|
||||
'Relay address and channel must exist together'): cv.byte,
|
||||
vol.Inclusive(CONF_RELAY_CHAN, 'relaylocation',
|
||||
|
||||
@@ -46,9 +46,7 @@ ALERT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NOTIFIERS): cv.ensure_list})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
cv.slug: ALERT_SCHEMA,
|
||||
}),
|
||||
DOMAIN: cv.schema_with_slug_keys(ALERT_SCHEMA),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@ from homeassistant.helpers import entityfilter
|
||||
|
||||
from . import flash_briefings, intent, smart_home
|
||||
from .const import (
|
||||
CONF_AUDIO, CONF_DISPLAY_URL, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN,
|
||||
CONF_FILTER, CONF_ENTITY_CONFIG)
|
||||
CONF_AUDIO, CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY_URL,
|
||||
CONF_ENDPOINT, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN, CONF_FILTER,
|
||||
CONF_ENTITY_CONFIG)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,6 +31,9 @@ ALEXA_ENTITY_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
SMART_HOME_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_ENDPOINT): cv.string,
|
||||
vol.Optional(CONF_CLIENT_ID): cv.string,
|
||||
vol.Optional(CONF_CLIENT_SECRET): cv.string,
|
||||
vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}
|
||||
})
|
||||
@@ -68,6 +72,6 @@ async def async_setup(hass, config):
|
||||
pass
|
||||
else:
|
||||
smart_home_config = smart_home_config or SMART_HOME_SCHEMA({})
|
||||
smart_home.async_setup(hass, smart_home_config)
|
||||
await smart_home.async_setup(hass, smart_home_config)
|
||||
|
||||
return True
|
||||
|
||||
154
homeassistant/components/alexa/auth.py
Normal file
154
homeassistant/components/alexa/auth.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""Support for Alexa skill auth."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.util import dt
|
||||
from .const import DEFAULT_TIMEOUT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
LWA_TOKEN_URI = "https://api.amazon.com/auth/o2/token"
|
||||
LWA_HEADERS = {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
|
||||
}
|
||||
|
||||
PREEMPTIVE_REFRESH_TTL_IN_SECONDS = 300
|
||||
STORAGE_KEY = 'alexa_auth'
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_EXPIRE_TIME = "expire_time"
|
||||
STORAGE_ACCESS_TOKEN = "access_token"
|
||||
STORAGE_REFRESH_TOKEN = "refresh_token"
|
||||
|
||||
|
||||
class Auth:
|
||||
"""Handle authentication to send events to Alexa."""
|
||||
|
||||
def __init__(self, hass, client_id, client_secret):
|
||||
"""Initialize the Auth class."""
|
||||
self.hass = hass
|
||||
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
|
||||
self._prefs = None
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
self._get_token_lock = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
async def async_do_auth(self, accept_grant_code):
|
||||
"""Do authentication with an AcceptGrant code."""
|
||||
# access token not retrieved yet for the first time, so this should
|
||||
# be an access token request
|
||||
|
||||
lwa_params = {
|
||||
"grant_type": "authorization_code",
|
||||
"code": accept_grant_code,
|
||||
"client_id": self.client_id,
|
||||
"client_secret": self.client_secret
|
||||
}
|
||||
_LOGGER.debug("Calling LWA to get the access token (first time), "
|
||||
"with: %s", json.dumps(lwa_params))
|
||||
|
||||
return await self._async_request_new_token(lwa_params)
|
||||
|
||||
async def async_get_access_token(self):
|
||||
"""Perform access token or token refresh request."""
|
||||
async with self._get_token_lock:
|
||||
if self._prefs is None:
|
||||
await self.async_load_preferences()
|
||||
|
||||
if self.is_token_valid():
|
||||
_LOGGER.debug("Token still valid, using it.")
|
||||
return self._prefs[STORAGE_ACCESS_TOKEN]
|
||||
|
||||
if self._prefs[STORAGE_REFRESH_TOKEN] is None:
|
||||
_LOGGER.debug("Token invalid and no refresh token available.")
|
||||
return None
|
||||
|
||||
lwa_params = {
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": self._prefs[STORAGE_REFRESH_TOKEN],
|
||||
"client_id": self.client_id,
|
||||
"client_secret": self.client_secret
|
||||
}
|
||||
|
||||
_LOGGER.debug("Calling LWA to refresh the access token.")
|
||||
return await self._async_request_new_token(lwa_params)
|
||||
|
||||
@callback
|
||||
def is_token_valid(self):
|
||||
"""Check if a token is already loaded and if it is still valid."""
|
||||
if not self._prefs[STORAGE_ACCESS_TOKEN]:
|
||||
return False
|
||||
|
||||
expire_time = dt.parse_datetime(self._prefs[STORAGE_EXPIRE_TIME])
|
||||
preemptive_expire_time = expire_time - timedelta(
|
||||
seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS)
|
||||
|
||||
return dt.utcnow() < preemptive_expire_time
|
||||
|
||||
async def _async_request_new_token(self, lwa_params):
|
||||
|
||||
try:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self.hass.loop):
|
||||
response = await session.post(LWA_TOKEN_URI,
|
||||
headers=LWA_HEADERS,
|
||||
data=lwa_params,
|
||||
allow_redirects=True)
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Timeout calling LWA to get auth token.")
|
||||
return None
|
||||
|
||||
_LOGGER.debug("LWA response header: %s", response.headers)
|
||||
_LOGGER.debug("LWA response status: %s", response.status)
|
||||
|
||||
if response.status != 200:
|
||||
_LOGGER.error("Error calling LWA to get auth token.")
|
||||
return None
|
||||
|
||||
response_json = await response.json()
|
||||
_LOGGER.debug("LWA response body : %s", response_json)
|
||||
|
||||
access_token = response_json["access_token"]
|
||||
refresh_token = response_json["refresh_token"]
|
||||
expires_in = response_json["expires_in"]
|
||||
expire_time = dt.utcnow() + timedelta(seconds=expires_in)
|
||||
|
||||
await self._async_update_preferences(access_token, refresh_token,
|
||||
expire_time.isoformat())
|
||||
|
||||
return access_token
|
||||
|
||||
async def async_load_preferences(self):
|
||||
"""Load preferences with stored tokens."""
|
||||
self._prefs = await self._store.async_load()
|
||||
|
||||
if self._prefs is None:
|
||||
self._prefs = {
|
||||
STORAGE_ACCESS_TOKEN: None,
|
||||
STORAGE_REFRESH_TOKEN: None,
|
||||
STORAGE_EXPIRE_TIME: None
|
||||
}
|
||||
|
||||
async def _async_update_preferences(self, access_token, refresh_token,
|
||||
expire_time):
|
||||
"""Update user preferences."""
|
||||
if self._prefs is None:
|
||||
await self.async_load_preferences()
|
||||
|
||||
if access_token is not None:
|
||||
self._prefs[STORAGE_ACCESS_TOKEN] = access_token
|
||||
if refresh_token is not None:
|
||||
self._prefs[STORAGE_REFRESH_TOKEN] = refresh_token
|
||||
if expire_time is not None:
|
||||
self._prefs[STORAGE_EXPIRE_TIME] = expire_time
|
||||
await self._store.async_save(self._prefs)
|
||||
@@ -10,6 +10,9 @@ CONF_DISPLAY_URL = 'display_url'
|
||||
|
||||
CONF_FILTER = 'filter'
|
||||
CONF_ENTITY_CONFIG = 'entity_config'
|
||||
CONF_ENDPOINT = 'endpoint'
|
||||
CONF_CLIENT_ID = 'client_id'
|
||||
CONF_CLIENT_SECRET = 'client_secret'
|
||||
|
||||
ATTR_UID = 'uid'
|
||||
ATTR_UPDATE_DATE = 'updateDate'
|
||||
@@ -21,3 +24,5 @@ ATTR_REDIRECTION_URL = 'redirectionURL'
|
||||
SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH'
|
||||
|
||||
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
|
||||
|
||||
DEFAULT_TIMEOUT = 30
|
||||
|
||||
@@ -5,29 +5,39 @@ https://developer.amazon.com/docs/smarthome/understand-the-smart-home-skill-api.
|
||||
https://developer.amazon.com/docs/device-apis/message-guide.html
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
from uuid import uuid4
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.components import (
|
||||
alert, automation, binary_sensor, climate, cover, fan, group, http,
|
||||
input_boolean, light, lock, media_player, scene, script, sensor, switch)
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CLOUD_NEVER_EXPOSED_ENTITIES,
|
||||
CONF_NAME, SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
|
||||
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
SERVICE_UNLOCK, SERVICE_VOLUME_SET, STATE_LOCKED, STATE_ON, STATE_UNLOCKED,
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
SERVICE_UNLOCK, SERVICE_VOLUME_SET, STATE_LOCKED, STATE_ON,
|
||||
STATE_UNAVAILABLE, STATE_UNLOCKED, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
MATCH_ALL)
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
|
||||
from .const import CONF_ENTITY_CONFIG, CONF_FILTER
|
||||
from .const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_ENDPOINT, \
|
||||
CONF_ENTITY_CONFIG, CONF_FILTER, DATE_FORMAT, DEFAULT_TIMEOUT
|
||||
from .auth import Auth
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,6 +47,8 @@ API_EVENT = 'event'
|
||||
API_CONTEXT = 'context'
|
||||
API_HEADER = 'header'
|
||||
API_PAYLOAD = 'payload'
|
||||
API_SCOPE = 'scope'
|
||||
API_CHANGE = 'change'
|
||||
|
||||
API_TEMP_UNITS = {
|
||||
TEMP_FAHRENHEIT: 'FAHRENHEIT',
|
||||
@@ -66,6 +78,8 @@ HANDLERS = Registry()
|
||||
ENTITY_ADAPTERS = Registry()
|
||||
EVENT_ALEXA_SMART_HOME = 'alexa_smart_home'
|
||||
|
||||
AUTH_KEY = "alexa.smart_home.auth"
|
||||
|
||||
|
||||
class _DisplayCategory:
|
||||
"""Possible display categories for Discovery response.
|
||||
@@ -375,9 +389,42 @@ class _AlexaInterface:
|
||||
'name': prop_name,
|
||||
'namespace': self.name(),
|
||||
'value': prop_value,
|
||||
'timeOfSample': datetime.now().strftime(DATE_FORMAT),
|
||||
'uncertaintyInMilliseconds': 0
|
||||
}
|
||||
|
||||
|
||||
class _AlexaEndpointHealth(_AlexaInterface):
|
||||
"""Implements Alexa.EndpointHealth.
|
||||
|
||||
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-when-alexa-requests-it
|
||||
"""
|
||||
|
||||
def __init__(self, hass, entity):
|
||||
super().__init__(entity)
|
||||
self.hass = hass
|
||||
|
||||
def name(self):
|
||||
return 'Alexa.EndpointHealth'
|
||||
|
||||
def properties_supported(self):
|
||||
return [{'name': 'connectivity'}]
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
return False
|
||||
|
||||
def properties_retrievable(self):
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
if name != 'connectivity':
|
||||
raise _UnsupportedProperty(name)
|
||||
|
||||
if self.entity.state == STATE_UNAVAILABLE:
|
||||
return {'value': 'UNREACHABLE'}
|
||||
return {'value': 'OK'}
|
||||
|
||||
|
||||
class _AlexaPowerController(_AlexaInterface):
|
||||
"""Implements Alexa.PowerController.
|
||||
|
||||
@@ -390,6 +437,9 @@ class _AlexaPowerController(_AlexaInterface):
|
||||
def properties_supported(self):
|
||||
return [{'name': 'powerState'}]
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
return True
|
||||
|
||||
def properties_retrievable(self):
|
||||
return True
|
||||
|
||||
@@ -417,6 +467,9 @@ class _AlexaLockController(_AlexaInterface):
|
||||
def properties_retrievable(self):
|
||||
return True
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
if name != 'lockState':
|
||||
raise _UnsupportedProperty(name)
|
||||
@@ -454,6 +507,9 @@ class _AlexaBrightnessController(_AlexaInterface):
|
||||
def properties_supported(self):
|
||||
return [{'name': 'brightness'}]
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
return True
|
||||
|
||||
def properties_retrievable(self):
|
||||
return True
|
||||
|
||||
@@ -585,6 +641,9 @@ class _AlexaTemperatureSensor(_AlexaInterface):
|
||||
def properties_supported(self):
|
||||
return [{'name': 'temperature'}]
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
return True
|
||||
|
||||
def properties_retrievable(self):
|
||||
return True
|
||||
|
||||
@@ -625,6 +684,9 @@ class _AlexaContactSensor(_AlexaInterface):
|
||||
def properties_supported(self):
|
||||
return [{'name': 'detectionState'}]
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
return True
|
||||
|
||||
def properties_retrievable(self):
|
||||
return True
|
||||
|
||||
@@ -648,6 +710,9 @@ class _AlexaMotionSensor(_AlexaInterface):
|
||||
def properties_supported(self):
|
||||
return [{'name': 'detectionState'}]
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
return True
|
||||
|
||||
def properties_retrievable(self):
|
||||
return True
|
||||
|
||||
@@ -686,6 +751,9 @@ class _AlexaThermostatController(_AlexaInterface):
|
||||
properties.append({'name': 'thermostatMode'})
|
||||
return properties
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
return True
|
||||
|
||||
def properties_retrievable(self):
|
||||
return True
|
||||
|
||||
@@ -733,7 +801,8 @@ class _GenericCapabilities(_AlexaEntity):
|
||||
return [_DisplayCategory.OTHER]
|
||||
|
||||
def interfaces(self):
|
||||
return [_AlexaPowerController(self.entity)]
|
||||
return [_AlexaPowerController(self.entity),
|
||||
_AlexaEndpointHealth(self.hass, self.entity)]
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(switch.DOMAIN)
|
||||
@@ -742,7 +811,8 @@ class _SwitchCapabilities(_AlexaEntity):
|
||||
return [_DisplayCategory.SWITCH]
|
||||
|
||||
def interfaces(self):
|
||||
return [_AlexaPowerController(self.entity)]
|
||||
return [_AlexaPowerController(self.entity),
|
||||
_AlexaEndpointHealth(self.hass, self.entity)]
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(climate.DOMAIN)
|
||||
@@ -756,6 +826,7 @@ class _ClimateCapabilities(_AlexaEntity):
|
||||
yield _AlexaPowerController(self.entity)
|
||||
yield _AlexaThermostatController(self.hass, self.entity)
|
||||
yield _AlexaTemperatureSensor(self.hass, self.entity)
|
||||
yield _AlexaEndpointHealth(self.hass, self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(cover.DOMAIN)
|
||||
@@ -768,6 +839,7 @@ class _CoverCapabilities(_AlexaEntity):
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & cover.SUPPORT_SET_POSITION:
|
||||
yield _AlexaPercentageController(self.entity)
|
||||
yield _AlexaEndpointHealth(self.hass, self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(light.DOMAIN)
|
||||
@@ -785,6 +857,7 @@ class _LightCapabilities(_AlexaEntity):
|
||||
yield _AlexaColorController(self.entity)
|
||||
if supported & light.SUPPORT_COLOR_TEMP:
|
||||
yield _AlexaColorTemperatureController(self.entity)
|
||||
yield _AlexaEndpointHealth(self.hass, self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(fan.DOMAIN)
|
||||
@@ -797,6 +870,7 @@ class _FanCapabilities(_AlexaEntity):
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & fan.SUPPORT_SET_SPEED:
|
||||
yield _AlexaPercentageController(self.entity)
|
||||
yield _AlexaEndpointHealth(self.hass, self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(lock.DOMAIN)
|
||||
@@ -805,7 +879,8 @@ class _LockCapabilities(_AlexaEntity):
|
||||
return [_DisplayCategory.SMARTLOCK]
|
||||
|
||||
def interfaces(self):
|
||||
return [_AlexaLockController(self.entity)]
|
||||
return [_AlexaLockController(self.entity),
|
||||
_AlexaEndpointHealth(self.hass, self.entity)]
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(media_player.DOMAIN)
|
||||
@@ -815,6 +890,7 @@ class _MediaPlayerCapabilities(_AlexaEntity):
|
||||
|
||||
def interfaces(self):
|
||||
yield _AlexaPowerController(self.entity)
|
||||
yield _AlexaEndpointHealth(self.hass, self.entity)
|
||||
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & media_player.SUPPORT_VOLUME_SET:
|
||||
@@ -877,6 +953,7 @@ class _SensorCapabilities(_AlexaEntity):
|
||||
TEMP_CELSIUS,
|
||||
):
|
||||
yield _AlexaTemperatureSensor(self.hass, self.entity)
|
||||
yield _AlexaEndpointHealth(self.hass, self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(binary_sensor.DOMAIN)
|
||||
@@ -898,6 +975,8 @@ class _BinarySensorCapabilities(_AlexaEntity):
|
||||
elif sensor_type is self.TYPE_MOTION:
|
||||
yield _AlexaMotionSensor(self.hass, self.entity)
|
||||
|
||||
yield _AlexaEndpointHealth(self.hass, self.entity)
|
||||
|
||||
def get_type(self):
|
||||
"""Return the type of binary sensor."""
|
||||
attrs = self.entity.attributes
|
||||
@@ -948,14 +1027,16 @@ class _Cause:
|
||||
class Config:
|
||||
"""Hold the configuration for Alexa."""
|
||||
|
||||
def __init__(self, should_expose, entity_config=None):
|
||||
def __init__(self, endpoint, async_get_access_token, should_expose,
|
||||
entity_config=None):
|
||||
"""Initialize the configuration."""
|
||||
self.endpoint = endpoint
|
||||
self.async_get_access_token = async_get_access_token
|
||||
self.should_expose = should_expose
|
||||
self.entity_config = entity_config or {}
|
||||
|
||||
|
||||
@ha.callback
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Activate Smart Home functionality of Alexa component.
|
||||
|
||||
This is optional, triggered by having a `smart_home:` sub-section in the
|
||||
@@ -964,12 +1045,61 @@ def async_setup(hass, config):
|
||||
Even if that's disabled, the functionality in this module may still be used
|
||||
by the cloud component which will call async_handle_message directly.
|
||||
"""
|
||||
if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET):
|
||||
hass.data[AUTH_KEY] = Auth(hass, config[CONF_CLIENT_ID],
|
||||
config[CONF_CLIENT_SECRET])
|
||||
|
||||
async_get_access_token = \
|
||||
hass.data[AUTH_KEY].async_get_access_token if AUTH_KEY in hass.data \
|
||||
else None
|
||||
|
||||
smart_home_config = Config(
|
||||
endpoint=config.get(CONF_ENDPOINT),
|
||||
async_get_access_token=async_get_access_token,
|
||||
should_expose=config[CONF_FILTER],
|
||||
entity_config=config.get(CONF_ENTITY_CONFIG),
|
||||
)
|
||||
hass.http.register_view(SmartHomeView(smart_home_config))
|
||||
|
||||
if AUTH_KEY in hass.data:
|
||||
await async_enable_proactive_mode(hass, smart_home_config)
|
||||
|
||||
|
||||
async def async_enable_proactive_mode(hass, smart_home_config):
|
||||
"""Enable the proactive mode.
|
||||
|
||||
Proactive mode makes this component report state changes to Alexa.
|
||||
"""
|
||||
if smart_home_config.async_get_access_token is None:
|
||||
# no function to call to get token
|
||||
return
|
||||
|
||||
if await smart_home_config.async_get_access_token() is None:
|
||||
# not ready yet
|
||||
return
|
||||
|
||||
async def async_entity_state_listener(changed_entity, old_state,
|
||||
new_state):
|
||||
if not smart_home_config.should_expose(changed_entity):
|
||||
_LOGGER.debug("Not exposing %s because filtered by config",
|
||||
changed_entity)
|
||||
return
|
||||
|
||||
if new_state.domain not in ENTITY_ADAPTERS:
|
||||
return
|
||||
|
||||
alexa_changed_entity = \
|
||||
ENTITY_ADAPTERS[new_state.domain](hass, smart_home_config,
|
||||
new_state)
|
||||
|
||||
for interface in alexa_changed_entity.interfaces():
|
||||
if interface.properties_proactively_reported():
|
||||
await async_send_changereport_message(hass, smart_home_config,
|
||||
alexa_changed_entity)
|
||||
return
|
||||
|
||||
async_track_state_change(hass, MATCH_ALL, async_entity_state_listener)
|
||||
|
||||
|
||||
class SmartHomeView(http.HomeAssistantView):
|
||||
"""Expose Smart Home v3 payload interface via HTTP POST."""
|
||||
@@ -1112,6 +1242,24 @@ class _AlexaResponse:
|
||||
"""
|
||||
self._response[API_EVENT][API_HEADER]['correlationToken'] = token
|
||||
|
||||
def set_endpoint_full(self, bearer_token, endpoint_id, cookie=None):
|
||||
"""Set the endpoint dictionary.
|
||||
|
||||
This is used to send proactive messages to Alexa.
|
||||
"""
|
||||
self._response[API_EVENT][API_ENDPOINT] = {
|
||||
API_SCOPE: {
|
||||
'type': 'BearerToken',
|
||||
'token': bearer_token
|
||||
}
|
||||
}
|
||||
|
||||
if endpoint_id is not None:
|
||||
self._response[API_EVENT][API_ENDPOINT]['endpointId'] = endpoint_id
|
||||
|
||||
if cookie is not None:
|
||||
self._response[API_EVENT][API_ENDPOINT]['cookie'] = cookie
|
||||
|
||||
def set_endpoint(self, endpoint):
|
||||
"""Set the endpoint.
|
||||
|
||||
@@ -1222,6 +1370,61 @@ async def async_handle_message(
|
||||
return response.serialize()
|
||||
|
||||
|
||||
async def async_send_changereport_message(hass, config, alexa_entity):
|
||||
"""Send a ChangeReport message for an Alexa entity."""
|
||||
token = await config.async_get_access_token()
|
||||
if not token:
|
||||
_LOGGER.error("Invalid access token.")
|
||||
return
|
||||
|
||||
headers = {
|
||||
"Authorization": "Bearer {}".format(token)
|
||||
}
|
||||
|
||||
endpoint = alexa_entity.entity_id()
|
||||
|
||||
# this sends all the properties of the Alexa Entity, whether they have
|
||||
# changed or not. this should be improved, and properties that have not
|
||||
# changed should be moved to the 'context' object
|
||||
properties = list(alexa_entity.serialize_properties())
|
||||
|
||||
payload = {
|
||||
API_CHANGE: {
|
||||
'cause': {'type': _Cause.APP_INTERACTION},
|
||||
'properties': properties
|
||||
}
|
||||
}
|
||||
|
||||
message = _AlexaResponse(name='ChangeReport', namespace='Alexa',
|
||||
payload=payload)
|
||||
message.set_endpoint_full(token, endpoint)
|
||||
|
||||
message_serialized = message.serialize()
|
||||
|
||||
try:
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
|
||||
response = await session.post(config.endpoint,
|
||||
headers=headers,
|
||||
json=message_serialized,
|
||||
allow_redirects=True)
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Timeout calling LWA to get auth token.")
|
||||
return None
|
||||
|
||||
response_text = await response.text()
|
||||
|
||||
_LOGGER.debug("Sent: %s", json.dumps(message_serialized))
|
||||
_LOGGER.debug("Received (%s): %s", response.status, response_text)
|
||||
|
||||
if response.status != 202:
|
||||
response_json = json.loads(response_text)
|
||||
_LOGGER.error("Error when sending ChangeReport to Alexa: %s: %s",
|
||||
response_json["payload"]["code"],
|
||||
response_json["payload"]["description"])
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
|
||||
async def async_api_discovery(hass, config, directive, context):
|
||||
"""Create a API formatted discovery response.
|
||||
@@ -1258,8 +1461,9 @@ async def async_api_discovery(hass, config, directive, context):
|
||||
i.serialize_discovery() for i in alexa_entity.interfaces()]
|
||||
|
||||
if not endpoint['capabilities']:
|
||||
_LOGGER.debug("Not exposing %s because it has no capabilities",
|
||||
entity.entity_id)
|
||||
_LOGGER.debug(
|
||||
"Not exposing %s because it has no capabilities",
|
||||
entity.entity_id)
|
||||
continue
|
||||
discovery_endpoints.append(endpoint)
|
||||
|
||||
@@ -1270,6 +1474,25 @@ async def async_api_discovery(hass, config, directive, context):
|
||||
)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Authorization', 'AcceptGrant'))
|
||||
async def async_api_accept_grant(hass, config, directive, context):
|
||||
"""Create a API formatted AcceptGrant response.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
auth_code = directive.payload['grant']['code']
|
||||
_LOGGER.debug("AcceptGrant code: %s", auth_code)
|
||||
|
||||
if AUTH_KEY in hass.data:
|
||||
await hass.data[AUTH_KEY].async_do_auth(auth_code)
|
||||
await async_enable_proactive_mode(hass, config)
|
||||
|
||||
return directive.response(
|
||||
name='AcceptGrant.Response',
|
||||
namespace='Alexa.Authorization',
|
||||
payload={})
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
|
||||
async def async_api_turn_on(hass, config, directive, context):
|
||||
"""Process a turn on request."""
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
REQUIREMENTS = ['pyarlo==0.2.2']
|
||||
REQUIREMENTS = ['pyarlo==0.2.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
|
||||
REQUIREMENTS = ['aioasuswrt==1.1.15']
|
||||
REQUIREMENTS = ['aioasuswrt==1.1.17']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,6 +26,8 @@ CONF_SSH_KEY = 'ssh_key'
|
||||
CONF_REQUIRE_IP = 'require_ip'
|
||||
DEFAULT_SSH_PORT = 22
|
||||
SECRET_GROUP = 'Password or SSH Key'
|
||||
CONF_SENSORS = 'sensors'
|
||||
SENSOR_TYPES = ['upload_speed', 'download_speed', 'download', 'upload']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
@@ -37,7 +39,9 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean,
|
||||
vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string,
|
||||
vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile,
|
||||
vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile
|
||||
vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile,
|
||||
vol.Optional(CONF_SENSORS): vol.All(
|
||||
cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -62,7 +66,8 @@ async def async_setup(hass, config):
|
||||
hass.data[DATA_ASUSWRT] = api
|
||||
|
||||
hass.async_create_task(async_load_platform(
|
||||
hass, 'sensor', DOMAIN, {}, config))
|
||||
hass, 'sensor', DOMAIN, config[DOMAIN].get(CONF_SENSORS), config))
|
||||
hass.async_create_task(async_load_platform(
|
||||
hass, 'device_tracker', DOMAIN, {}, config))
|
||||
|
||||
return True
|
||||
|
||||
@@ -5,28 +5,28 @@
|
||||
"no_available_service": "No hi ha serveis de notificaci\u00f3 disponibles."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho."
|
||||
"invalid_code": "Codi inv\u00e0lid, si us plau torna a provar-ho."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Seleccioneu un dels serveis de notificaci\u00f3:",
|
||||
"title": "Configureu una contrasenya d'un sol \u00fas a trav\u00e9s del component de notificacions"
|
||||
"description": "Selecciona un dels serveis de notificaci\u00f3:",
|
||||
"title": "Configuraci\u00f3 d'una contrasenya d'un sol \u00fas a trav\u00e9s del component de notificacions"
|
||||
},
|
||||
"setup": {
|
||||
"description": "S'ha enviat una contrasenya d'un sol \u00fas mitjan\u00e7ant **notify.{notify_service}**. Introdu\u00efu-la a continuaci\u00f3:",
|
||||
"title": "Verifiqueu la configuraci\u00f3"
|
||||
"description": "S'ha enviat una contrasenya d'un sol \u00fas mitjan\u00e7ant **notify.{notify_service}**. Introdueix-la a continuaci\u00f3:",
|
||||
"title": "Verificaci\u00f3 de la configuraci\u00f3"
|
||||
}
|
||||
},
|
||||
"title": "Contrasenya d'un sol \u00fas del servei de notificacions"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa."
|
||||
"invalid_code": "Codi inv\u00e0lid, si us plau torna a provar-ho. Si obtens aquest error repetidament, assegura't que la data i hora de Home Assistant siguin correctes i acurades."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Per activar la verificaci\u00f3 en dos passos mitjan\u00e7ant contrasenyes d'un sol \u00fas basades en temps, escanegeu el codi QR amb la vostre aplicaci\u00f3 de verificaci\u00f3. Si no en teniu cap, us recomanem [Google Authenticator](https://support.google.com/accounts/answer/1066447) o b\u00e9 [Authy](https://authy.com/). \n\n {qr_code} \n \nDespr\u00e9s d'escanejar el codi QR, introdu\u00efu el codi de sis d\u00edgits proporcionat per l'aplicaci\u00f3. Si teniu problemes per escanejar el codi QR, feu una configuraci\u00f3 manual amb el codi **`{code}`**.",
|
||||
"title": "Configureu la verificaci\u00f3 en dos passos utilitzant TOTP"
|
||||
"description": "Per activar la verificaci\u00f3 en dos passos mitjan\u00e7ant contrasenyes d'un sol \u00fas basades en temps, escaneja el codi QR amb la teva aplicaci\u00f3 de verificaci\u00f3. Si no en tens cap, et recomanem [Google Authenticator](https://support.google.com/accounts/answer/1066447) o b\u00e9 [Authy](https://authy.com/). \n\n {qr_code} \n \nDespr\u00e9s d'escanejar el codi QR, introdueix el codi de sis d\u00edgits proporcionat per l'aplicaci\u00f3. Si tens problemes per escanejar el codi QR, fes una configuraci\u00f3 manual amb el codi **`{code}`**.",
|
||||
"title": "Configura la verificaci\u00f3 en dos passos utilitzant TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Bitte w\u00e4hle einen der Benachrichtigungsdienste:",
|
||||
"description": "Bitte w\u00e4hlen Sie einen der Benachrichtigungsdienste:",
|
||||
"title": "Einmal Passwort f\u00fcr Notify einrichten"
|
||||
},
|
||||
"setup": {
|
||||
"description": "Ein Einmal-Passwort wurde per ** notify gesendet. {notify_service} **. Bitte gebe es unten ein:",
|
||||
"description": "Ein Einmal-Passwort wurde per **notify.{notify_service}** gesendet. Bitte geben Sie es unten ein:",
|
||||
"title": "\u00dcberpr\u00fcfe das Setup"
|
||||
}
|
||||
},
|
||||
|
||||
7
homeassistant/components/auth/.translations/et.json
Normal file
7
homeassistant/components/auth/.translations/et.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"title": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,11 +94,11 @@ PLATFORM_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
})
|
||||
|
||||
TRIGGER_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
vol.Optional(ATTR_VARIABLES, default={}): dict,
|
||||
})
|
||||
|
||||
@@ -375,7 +375,13 @@ def _async_get_action(hass, config, name):
|
||||
async def action(entity_id, variables, context):
|
||||
"""Execute an action."""
|
||||
_LOGGER.info('Executing %s', name)
|
||||
await script_obj.async_run(variables, context)
|
||||
|
||||
try:
|
||||
await script_obj.async_run(variables, context)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
script_obj.async_log_exception(
|
||||
_LOGGER,
|
||||
'Error while executing automation {}'.format(entity_id), err)
|
||||
|
||||
return action
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""
|
||||
Offer geo location automation rules.
|
||||
Offer geolocation automation rules.
|
||||
|
||||
For more details about this automation trigger, please refer to the
|
||||
documentation at
|
||||
https://home-assistant.io/docs/automation/trigger/#geo-location-trigger
|
||||
https://home-assistant.io/docs/automation/trigger/#geolocation-trigger
|
||||
"""
|
||||
import voluptuous as vol
|
||||
|
||||
|
||||
@@ -13,30 +13,18 @@ from homeassistant.const import CONF_AT, CONF_PLATFORM
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_change
|
||||
|
||||
CONF_HOURS = 'hours'
|
||||
CONF_MINUTES = 'minutes'
|
||||
CONF_SECONDS = 'seconds'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'time',
|
||||
CONF_AT: cv.time,
|
||||
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT))
|
||||
vol.Required(CONF_AT): cv.time,
|
||||
})
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action, automation_info):
|
||||
"""Listen for state changes based on configuration."""
|
||||
if CONF_AT in config:
|
||||
at_time = config.get(CONF_AT)
|
||||
hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second
|
||||
else:
|
||||
hours = config.get(CONF_HOURS)
|
||||
minutes = config.get(CONF_MINUTES)
|
||||
seconds = config.get(CONF_SECONDS)
|
||||
at_time = config.get(CONF_AT)
|
||||
hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second
|
||||
|
||||
@callback
|
||||
def time_automation_listener(now):
|
||||
|
||||
53
homeassistant/components/automation/time_pattern.py
Normal file
53
homeassistant/components/automation/time_pattern.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Offer time listening automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/docs/automation/trigger/#time-trigger
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_change
|
||||
|
||||
CONF_HOURS = 'hours'
|
||||
CONF_MINUTES = 'minutes'
|
||||
CONF_SECONDS = 'seconds'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'time_pattern',
|
||||
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS))
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action, automation_info):
|
||||
"""Listen for state changes based on configuration."""
|
||||
hours = config.get(CONF_HOURS)
|
||||
minutes = config.get(CONF_MINUTES)
|
||||
seconds = config.get(CONF_SECONDS)
|
||||
|
||||
# If larger units are specified, default the smaller units to zero
|
||||
if minutes is None and hours is not None:
|
||||
minutes = 0
|
||||
if seconds is None and minutes is not None:
|
||||
seconds = 0
|
||||
|
||||
@callback
|
||||
def time_automation_listener(now):
|
||||
"""Listen for time changes and calls action."""
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'time_pattern',
|
||||
'now': now,
|
||||
},
|
||||
})
|
||||
|
||||
return async_track_time_change(hass, time_automation_listener,
|
||||
hour=hours, minute=minutes, second=seconds)
|
||||
@@ -50,9 +50,7 @@ DEVICE_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
cv.slug: DEVICE_SCHEMA,
|
||||
}),
|
||||
DOMAIN: cv.schema_with_slug_keys(DEVICE_SCHEMA),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SERVICE_VAPIX_CALL = 'vapix_call'
|
||||
|
||||
@@ -9,7 +9,7 @@ import logging
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.alarmdecoder import (
|
||||
ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
|
||||
CONF_ZONE_RFID, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE,
|
||||
CONF_ZONE_RFID, CONF_ZONE_LOOP, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE,
|
||||
SIGNAL_RFX_MESSAGE, SIGNAL_REL_MESSAGE, CONF_RELAY_ADDR,
|
||||
CONF_RELAY_CHAN)
|
||||
|
||||
@@ -37,10 +37,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
zone_type = device_config_data[CONF_ZONE_TYPE]
|
||||
zone_name = device_config_data[CONF_ZONE_NAME]
|
||||
zone_rfid = device_config_data.get(CONF_ZONE_RFID)
|
||||
zone_loop = device_config_data.get(CONF_ZONE_LOOP)
|
||||
relay_addr = device_config_data.get(CONF_RELAY_ADDR)
|
||||
relay_chan = device_config_data.get(CONF_RELAY_CHAN)
|
||||
device = AlarmDecoderBinarySensor(
|
||||
zone_num, zone_name, zone_type, zone_rfid, relay_addr, relay_chan)
|
||||
zone_num, zone_name, zone_type, zone_rfid, zone_loop, relay_addr,
|
||||
relay_chan)
|
||||
devices.append(device)
|
||||
|
||||
add_entities(devices)
|
||||
@@ -51,7 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an AlarmDecoder binary sensor."""
|
||||
|
||||
def __init__(self, zone_number, zone_name, zone_type, zone_rfid,
|
||||
def __init__(self, zone_number, zone_name, zone_type, zone_rfid, zone_loop,
|
||||
relay_addr, relay_chan):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._zone_number = zone_number
|
||||
@@ -59,6 +61,7 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
self._state = None
|
||||
self._name = zone_name
|
||||
self._rfid = zone_rfid
|
||||
self._loop = zone_loop
|
||||
self._rfstate = None
|
||||
self._relay_addr = relay_addr
|
||||
self._relay_chan = relay_chan
|
||||
@@ -92,14 +95,14 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
"""Return the state attributes."""
|
||||
attr = {}
|
||||
if self._rfid and self._rfstate is not None:
|
||||
attr[ATTR_RF_BIT0] = True if self._rfstate & 0x01 else False
|
||||
attr[ATTR_RF_LOW_BAT] = True if self._rfstate & 0x02 else False
|
||||
attr[ATTR_RF_SUPERVISED] = True if self._rfstate & 0x04 else False
|
||||
attr[ATTR_RF_BIT3] = True if self._rfstate & 0x08 else False
|
||||
attr[ATTR_RF_LOOP3] = True if self._rfstate & 0x10 else False
|
||||
attr[ATTR_RF_LOOP2] = True if self._rfstate & 0x20 else False
|
||||
attr[ATTR_RF_LOOP4] = True if self._rfstate & 0x40 else False
|
||||
attr[ATTR_RF_LOOP1] = True if self._rfstate & 0x80 else False
|
||||
attr[ATTR_RF_BIT0] = bool(self._rfstate & 0x01)
|
||||
attr[ATTR_RF_LOW_BAT] = bool(self._rfstate & 0x02)
|
||||
attr[ATTR_RF_SUPERVISED] = bool(self._rfstate & 0x04)
|
||||
attr[ATTR_RF_BIT3] = bool(self._rfstate & 0x08)
|
||||
attr[ATTR_RF_LOOP3] = bool(self._rfstate & 0x10)
|
||||
attr[ATTR_RF_LOOP2] = bool(self._rfstate & 0x20)
|
||||
attr[ATTR_RF_LOOP4] = bool(self._rfstate & 0x40)
|
||||
attr[ATTR_RF_LOOP1] = bool(self._rfstate & 0x80)
|
||||
return attr
|
||||
|
||||
@property
|
||||
@@ -128,6 +131,8 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
"""Update RF state."""
|
||||
if self._rfid and message and message.serial_number == self._rfid:
|
||||
self._rfstate = message.value
|
||||
if self._loop:
|
||||
self._state = 1 if message.loop[self._loop - 1] else 0
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _rel_message_callback(self, message):
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
"""
|
||||
Support for deCONZ binary sensor.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.deconz/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.deconz.const import (
|
||||
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DECONZ_REACHABLE,
|
||||
DOMAIN as DECONZ_DOMAIN)
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
DEPENDENCIES = ['deconz']
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Old way of setting up deCONZ binary sensors."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the deCONZ binary sensor."""
|
||||
gateway = hass.data[DECONZ_DOMAIN]
|
||||
|
||||
@callback
|
||||
def async_add_sensor(sensors):
|
||||
"""Add binary sensor from deCONZ."""
|
||||
from pydeconz.sensor import DECONZ_BINARY_SENSOR
|
||||
entities = []
|
||||
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
|
||||
for sensor in sensors:
|
||||
if sensor.type in DECONZ_BINARY_SENSOR and \
|
||||
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
|
||||
entities.append(DeconzBinarySensor(sensor, gateway))
|
||||
async_add_entities(entities, True)
|
||||
|
||||
gateway.listeners.append(
|
||||
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
|
||||
|
||||
async_add_sensor(gateway.api.sensors.values())
|
||||
|
||||
|
||||
class DeconzBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a binary sensor."""
|
||||
|
||||
def __init__(self, sensor, gateway):
|
||||
"""Set up sensor and add update callback to get data from websocket."""
|
||||
self._sensor = sensor
|
||||
self.gateway = gateway
|
||||
self.unsub_dispatcher = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe sensors events."""
|
||||
self._sensor.register_async_callback(self.async_update_callback)
|
||||
self.gateway.deconz_ids[self.entity_id] = self._sensor.deconz_id
|
||||
self.unsub_dispatcher = async_dispatcher_connect(
|
||||
self.hass, DECONZ_REACHABLE, self.async_update_callback)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect sensor object when removed."""
|
||||
if self.unsub_dispatcher is not None:
|
||||
self.unsub_dispatcher()
|
||||
self._sensor.remove_callback(self.async_update_callback)
|
||||
self._sensor = None
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, reason):
|
||||
"""Update the sensor's state.
|
||||
|
||||
If reason is that state is updated,
|
||||
or reachable has changed or battery has changed.
|
||||
"""
|
||||
if reason['state'] or \
|
||||
'reachable' in reason['attr'] or \
|
||||
'battery' in reason['attr'] or \
|
||||
'on' in reason['attr']:
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._sensor.is_tripped
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._sensor.name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique identifier for this sensor."""
|
||||
return self._sensor.uniqueid
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the sensor."""
|
||||
return self._sensor.sensor_class
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend."""
|
||||
return self._sensor.sensor_icon
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if sensor is available."""
|
||||
return self.gateway.available and self._sensor.reachable
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
from pydeconz.sensor import PRESENCE
|
||||
attr = {}
|
||||
if self._sensor.battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
|
||||
if self._sensor.on is not None:
|
||||
attr[ATTR_ON] = self._sensor.on
|
||||
if self._sensor.type in PRESENCE and self._sensor.dark is not None:
|
||||
attr[ATTR_DARK] = self._sensor.dark
|
||||
return attr
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return a device description for device registry."""
|
||||
if (self._sensor.uniqueid is None or
|
||||
self._sensor.uniqueid.count(':') != 7):
|
||||
return None
|
||||
serial = self._sensor.uniqueid.split('-', 1)[0]
|
||||
bridgeid = self.gateway.api.config.bridgeid
|
||||
return {
|
||||
'connections': {(CONNECTION_ZIGBEE, serial)},
|
||||
'identifiers': {(DECONZ_DOMAIN, serial)},
|
||||
'manufacturer': self._sensor.manufacturer,
|
||||
'model': self._sensor.modelid,
|
||||
'name': self._sensor.name,
|
||||
'sw_version': self._sensor.swversion,
|
||||
'via_hub': (DECONZ_DOMAIN, bridgeid),
|
||||
}
|
||||
@@ -9,7 +9,8 @@ import logging
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, ENTITY_ID_FORMAT)
|
||||
from homeassistant.components.fibaro import (
|
||||
FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice)
|
||||
FIBARO_DEVICES, FibaroDevice)
|
||||
from homeassistant.const import (CONF_DEVICE_CLASS, CONF_ICON)
|
||||
|
||||
DEPENDENCIES = ['fibaro']
|
||||
|
||||
@@ -32,19 +33,20 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
return
|
||||
|
||||
add_entities(
|
||||
[FibaroBinarySensor(device, hass.data[FIBARO_CONTROLLER])
|
||||
[FibaroBinarySensor(device)
|
||||
for device in hass.data[FIBARO_DEVICES]['binary_sensor']], True)
|
||||
|
||||
|
||||
class FibaroBinarySensor(FibaroDevice, BinarySensorDevice):
|
||||
"""Representation of a Fibaro Binary Sensor."""
|
||||
|
||||
def __init__(self, fibaro_device, controller):
|
||||
def __init__(self, fibaro_device):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._state = None
|
||||
super().__init__(fibaro_device, controller)
|
||||
super().__init__(fibaro_device)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
|
||||
stype = None
|
||||
devconf = fibaro_device.device_config
|
||||
if fibaro_device.type in SENSOR_TYPES:
|
||||
stype = fibaro_device.type
|
||||
elif fibaro_device.baseType in SENSOR_TYPES:
|
||||
@@ -55,6 +57,10 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorDevice):
|
||||
else:
|
||||
self._device_class = None
|
||||
self._icon = None
|
||||
# device_config overrides:
|
||||
self._device_class = devconf.get(CONF_DEVICE_CLASS,
|
||||
self._device_class)
|
||||
self._icon = devconf.get(CONF_ICON, self._icon)
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
|
||||
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
|
||||
REQUIREMENTS = ['pyhik==0.1.8']
|
||||
REQUIREMENTS = ['pyhik==0.1.9']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IGNORED = 'ignored'
|
||||
|
||||
@@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.hive/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.hive import DATA_HIVE
|
||||
from homeassistant.components.hive import DATA_HIVE, DOMAIN
|
||||
|
||||
DEPENDENCIES = ['hive']
|
||||
|
||||
@@ -35,9 +35,24 @@ class HiveBinarySensorEntity(BinarySensorDevice):
|
||||
self.attributes = {}
|
||||
self.data_updatesource = '{}.{}'.format(self.device_type,
|
||||
self.node_id)
|
||||
|
||||
self._unique_id = '{}-{}'.format(self.node_id, self.device_type)
|
||||
self.session.entities.append(self)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique ID of entity."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device information."""
|
||||
return {
|
||||
'identifiers': {
|
||||
(DOMAIN, self.unique_id)
|
||||
},
|
||||
'name': self.name
|
||||
}
|
||||
|
||||
def handle_update(self, updatesource):
|
||||
"""Handle the new update request."""
|
||||
if '{}.{}'.format(self.device_type, self.node_id) not in updatesource:
|
||||
|
||||
@@ -28,14 +28,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the HomematicIP Cloud binary sensor from a config entry."""
|
||||
from homematicip.aio.device import (
|
||||
AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector,
|
||||
AsyncWaterSensor, AsyncRotaryHandleSensor)
|
||||
AsyncWaterSensor, AsyncRotaryHandleSensor,
|
||||
AsyncMotionDetectorPushButton)
|
||||
|
||||
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||
devices = []
|
||||
for device in home.devices:
|
||||
if isinstance(device, (AsyncShutterContact, AsyncRotaryHandleSensor)):
|
||||
devices.append(HomematicipShutterContact(home, device))
|
||||
elif isinstance(device, AsyncMotionDetectorIndoor):
|
||||
elif isinstance(device, (AsyncMotionDetectorIndoor,
|
||||
AsyncMotionDetectorPushButton)):
|
||||
devices.append(HomematicipMotionDetector(home, device))
|
||||
elif isinstance(device, AsyncSmokeDetector):
|
||||
devices.append(HomematicipSmokeDetector(home, device))
|
||||
|
||||
@@ -14,6 +14,7 @@ DEPENDENCIES = ['insteon']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSOR_TYPES = {'openClosedSensor': 'opening',
|
||||
'ioLincSensor': 'opening',
|
||||
'motionSensor': 'motion',
|
||||
'doorSensor': 'door',
|
||||
'wetLeakSensor': 'moisture',
|
||||
@@ -58,7 +59,7 @@ class InsteonBinarySensor(InsteonEntity, BinarySensorDevice):
|
||||
on_val = bool(self._insteon_device_state.value)
|
||||
|
||||
if self._insteon_device_state.name in ['lightSensor',
|
||||
'openClosedSensor']:
|
||||
'ioLincSensor']:
|
||||
return not on_val
|
||||
|
||||
return on_val
|
||||
|
||||
@@ -52,7 +52,7 @@ def setup_platform(hass, config: ConfigType,
|
||||
node.nid, node.parent_nid)
|
||||
else:
|
||||
device_type = _detect_device_type(node)
|
||||
subnode_id = int(node.nid[-1])
|
||||
subnode_id = int(node.nid[-1], 16)
|
||||
if device_type in ('opening', 'moisture'):
|
||||
# These sensors use an optional "negative" subnode 2 to snag
|
||||
# all state changes
|
||||
|
||||
@@ -61,8 +61,7 @@ class MyStromView(HomeAssistantView):
|
||||
'{}_{}'.format(button_id, button_action))
|
||||
self.add_entities([self.buttons[entity_id]])
|
||||
else:
|
||||
new_state = True if self.buttons[entity_id].state == 'off' \
|
||||
else False
|
||||
new_state = self.buttons[entity_id].state == 'off'
|
||||
self.buttons[entity_id].async_on_update(new_state)
|
||||
|
||||
|
||||
|
||||
81
homeassistant/components/binary_sensor/ness_alarm.py
Normal file
81
homeassistant/components/binary_sensor/ness_alarm.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Support for Ness D8X/D16X zone states - represented as binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.ness_alarm/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.ness_alarm import (
|
||||
CONF_ZONES, CONF_ZONE_TYPE, CONF_ZONE_NAME, CONF_ZONE_ID,
|
||||
SIGNAL_ZONE_CHANGED, ZoneChangedData)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
DEPENDENCIES = ['ness_alarm']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the Ness Alarm binary sensor devices."""
|
||||
if not discovery_info:
|
||||
return
|
||||
|
||||
configured_zones = discovery_info[CONF_ZONES]
|
||||
|
||||
devices = []
|
||||
|
||||
for zone_config in configured_zones:
|
||||
zone_type = zone_config[CONF_ZONE_TYPE]
|
||||
zone_name = zone_config[CONF_ZONE_NAME]
|
||||
zone_id = zone_config[CONF_ZONE_ID]
|
||||
device = NessZoneBinarySensor(zone_id=zone_id, name=zone_name,
|
||||
zone_type=zone_type)
|
||||
devices.append(device)
|
||||
|
||||
async_add_entities(devices)
|
||||
|
||||
|
||||
class NessZoneBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an Ness alarm zone as a binary sensor."""
|
||||
|
||||
def __init__(self, zone_id, name, zone_type):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._zone_id = zone_id
|
||||
self._name = name
|
||||
self._type = zone_type
|
||||
self._state = 0
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_ZONE_CHANGED, self._handle_zone_change)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._state == 1
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._type
|
||||
|
||||
@callback
|
||||
def _handle_zone_change(self, data: ZoneChangedData):
|
||||
"""Handle zone state update."""
|
||||
if self._zone_id == data.zone_id:
|
||||
self._state = data.state
|
||||
self.async_schedule_update_ha_state()
|
||||
@@ -7,8 +7,7 @@ https://home-assistant.io/components/binary_sensor.point/
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as PARENT_DOMAIN, BinarySensorDevice)
|
||||
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
|
||||
from homeassistant.components.point import MinutPointEntity
|
||||
from homeassistant.components.point.const import (
|
||||
DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW, SIGNAL_WEBHOOK)
|
||||
@@ -49,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
for device_class in EVENTS), True)
|
||||
|
||||
async_dispatcher_connect(
|
||||
hass, POINT_DISCOVERY_NEW.format(PARENT_DOMAIN, POINT_DOMAIN),
|
||||
hass, POINT_DISCOVERY_NEW.format(DOMAIN, POINT_DOMAIN),
|
||||
async_discover_sensor)
|
||||
|
||||
|
||||
|
||||
@@ -8,9 +8,11 @@ import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.satel_integra import (CONF_ZONES,
|
||||
CONF_OUTPUTS,
|
||||
CONF_ZONE_NAME,
|
||||
CONF_ZONE_TYPE,
|
||||
SIGNAL_ZONES_UPDATED)
|
||||
SIGNAL_ZONES_UPDATED,
|
||||
SIGNAL_OUTPUTS_UPDATED)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
@@ -32,7 +34,17 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
for zone_num, device_config_data in configured_zones.items():
|
||||
zone_type = device_config_data[CONF_ZONE_TYPE]
|
||||
zone_name = device_config_data[CONF_ZONE_NAME]
|
||||
device = SatelIntegraBinarySensor(zone_num, zone_name, zone_type)
|
||||
device = SatelIntegraBinarySensor(zone_num, zone_name, zone_type,
|
||||
SIGNAL_ZONES_UPDATED)
|
||||
devices.append(device)
|
||||
|
||||
configured_outputs = discovery_info[CONF_OUTPUTS]
|
||||
|
||||
for zone_num, device_config_data in configured_outputs.items():
|
||||
zone_type = device_config_data[CONF_ZONE_TYPE]
|
||||
zone_name = device_config_data[CONF_ZONE_NAME]
|
||||
device = SatelIntegraBinarySensor(zone_num, zone_name, zone_type,
|
||||
SIGNAL_OUTPUTS_UPDATED)
|
||||
devices.append(device)
|
||||
|
||||
async_add_entities(devices)
|
||||
@@ -41,17 +53,18 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
class SatelIntegraBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an Satel Integra binary sensor."""
|
||||
|
||||
def __init__(self, zone_number, zone_name, zone_type):
|
||||
def __init__(self, device_number, device_name, zone_type, react_to_signal):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._zone_number = zone_number
|
||||
self._name = zone_name
|
||||
self._device_number = device_number
|
||||
self._name = device_name
|
||||
self._zone_type = zone_type
|
||||
self._state = 0
|
||||
self._react_to_signal = react_to_signal
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_ZONES_UPDATED, self._zones_updated)
|
||||
self.hass, self._react_to_signal, self._devices_updated)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -80,9 +93,9 @@ class SatelIntegraBinarySensor(BinarySensorDevice):
|
||||
return self._zone_type
|
||||
|
||||
@callback
|
||||
def _zones_updated(self, zones):
|
||||
def _devices_updated(self, zones):
|
||||
"""Update the zone's state, if needed."""
|
||||
if self._zone_number in zones \
|
||||
and self._state != zones[self._zone_number]:
|
||||
self._state = zones[self._zone_number]
|
||||
if self._device_number in zones \
|
||||
and self._state != zones[self._device_number]:
|
||||
self._state = zones[self._device_number]
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@@ -9,22 +9,35 @@ https://home-assistant.io/components/binary_sensor.tellduslive/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components import tellduslive
|
||||
from homeassistant.components import binary_sensor, tellduslive
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.tellduslive.entry import TelldusLiveEntity
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up Tellstick sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
client = hass.data[tellduslive.DOMAIN]
|
||||
add_entities(
|
||||
TelldusLiveSensor(client, binary_sensor)
|
||||
for binary_sensor in discovery_info
|
||||
)
|
||||
"""Old way of setting up TelldusLive.
|
||||
|
||||
Can only be called when a user accidentally mentions the platform in their
|
||||
config. But even in that case it would have been ignored.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up tellduslive sensors dynamically."""
|
||||
async def async_discover_binary_sensor(device_id):
|
||||
"""Discover and add a discovered sensor."""
|
||||
client = hass.data[tellduslive.DOMAIN]
|
||||
async_add_entities([TelldusLiveSensor(client, device_id)])
|
||||
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
tellduslive.TELLDUS_DISCOVERY_NEW.format(binary_sensor.DOMAIN,
|
||||
tellduslive.DOMAIN),
|
||||
async_discover_binary_sensor)
|
||||
|
||||
|
||||
class TelldusLiveSensor(TelldusLiveEntity, BinarySensorDevice):
|
||||
|
||||
@@ -41,7 +41,7 @@ SENSOR_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}),
|
||||
vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA),
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ SENSOR_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}),
|
||||
vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA),
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ Support for WeMo sensors.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.wemo/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import async_timeout
|
||||
import requests
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
@@ -15,7 +18,7 @@ DEPENDENCIES = ['wemo']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities_callback, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Register discovered WeMo binary sensors."""
|
||||
from pywemo import discovery
|
||||
|
||||
@@ -31,7 +34,7 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None):
|
||||
raise PlatformNotReady
|
||||
|
||||
if device:
|
||||
add_entities_callback([WemoBinarySensor(hass, device)])
|
||||
add_entities([WemoBinarySensor(hass, device)])
|
||||
|
||||
|
||||
class WemoBinarySensor(BinarySensorDevice):
|
||||
@@ -41,48 +44,90 @@ class WemoBinarySensor(BinarySensorDevice):
|
||||
"""Initialize the WeMo sensor."""
|
||||
self.wemo = device
|
||||
self._state = None
|
||||
self._available = True
|
||||
self._update_lock = None
|
||||
self._model_name = self.wemo.model_name
|
||||
self._name = self.wemo.name
|
||||
self._serialnumber = self.wemo.serialnumber
|
||||
|
||||
wemo = hass.components.wemo
|
||||
wemo.SUBSCRIPTION_REGISTRY.register(self.wemo)
|
||||
wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback)
|
||||
|
||||
def _update_callback(self, _device, _type, _params):
|
||||
"""Handle state changes."""
|
||||
_LOGGER.info("Subscription update for %s", _device)
|
||||
def _subscription_callback(self, _device, _type, _params):
|
||||
"""Update the state by the Wemo sensor."""
|
||||
_LOGGER.debug("Subscription update for %s", self.name)
|
||||
updated = self.wemo.subscription_update(_type, _params)
|
||||
self._update(force_update=(not updated))
|
||||
self.hass.add_job(
|
||||
self._async_locked_subscription_callback(not updated))
|
||||
|
||||
if not hasattr(self, 'hass'):
|
||||
async def _async_locked_subscription_callback(self, force_update):
|
||||
"""Handle an update from a subscription."""
|
||||
# If an update is in progress, we don't do anything
|
||||
if self._update_lock.locked():
|
||||
return
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed with subscriptions."""
|
||||
return False
|
||||
await self._async_locked_update(force_update)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Wemo sensor added to HASS."""
|
||||
# Define inside async context so we know our event loop
|
||||
self._update_lock = asyncio.Lock()
|
||||
|
||||
registry = self.hass.components.wemo.SUBSCRIPTION_REGISTRY
|
||||
await self.hass.async_add_executor_job(registry.register, self.wemo)
|
||||
registry.on(self.wemo, None, self._subscription_callback)
|
||||
|
||||
async def async_update(self):
|
||||
"""Update WeMo state.
|
||||
|
||||
Wemo has an aggressive retry logic that sometimes can take over a
|
||||
minute to return. If we don't get a state after 5 seconds, assume the
|
||||
Wemo sensor is unreachable. If update goes through, it will be made
|
||||
available again.
|
||||
"""
|
||||
# If an update is in progress, we don't do anything
|
||||
if self._update_lock.locked():
|
||||
return
|
||||
|
||||
try:
|
||||
with async_timeout.timeout(5):
|
||||
await asyncio.shield(self._async_locked_update(True))
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.warning('Lost connection to %s', self.name)
|
||||
self._available = False
|
||||
|
||||
async def _async_locked_update(self, force_update):
|
||||
"""Try updating within an async lock."""
|
||||
async with self._update_lock:
|
||||
await self.hass.async_add_executor_job(self._update, force_update)
|
||||
|
||||
def _update(self, force_update=True):
|
||||
"""Update the sensor state."""
|
||||
try:
|
||||
self._state = self.wemo.get_state(force_update)
|
||||
|
||||
if not self._available:
|
||||
_LOGGER.info('Reconnected to %s', self.name)
|
||||
self._available = True
|
||||
except AttributeError as err:
|
||||
_LOGGER.warning("Could not update status for %s (%s)",
|
||||
self.name, err)
|
||||
self._available = False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the id of this WeMo device."""
|
||||
return self.wemo.serialnumber
|
||||
"""Return the id of this WeMo sensor."""
|
||||
return self._serialnumber
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the service if any."""
|
||||
return self.wemo.name
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update WeMo state."""
|
||||
self._update(force_update=True)
|
||||
|
||||
def _update(self, force_update=True):
|
||||
try:
|
||||
self._state = self.wemo.get_state(force_update)
|
||||
except AttributeError as err:
|
||||
_LOGGER.warning(
|
||||
"Could not update status for %s (%s)", self.name, err)
|
||||
@property
|
||||
def available(self):
|
||||
"""Return true if sensor is available."""
|
||||
return self._available
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.const import CONF_NAME, WEEKDAYS
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['holidays==0.9.8']
|
||||
REQUIREMENTS = ['holidays==0.9.9']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,6 +26,7 @@ ALL_COUNTRIES = [
|
||||
'Canada', 'CA', 'Colombia', 'CO', 'Croatia', 'HR', 'Czech', 'CZ',
|
||||
'Denmark', 'DK', 'England', 'EuropeanCentralBank', 'ECB', 'TAR',
|
||||
'Finland', 'FI', 'France', 'FRA', 'Germany', 'DE', 'Hungary', 'HU',
|
||||
'Honduras', 'HUD',
|
||||
'India', 'IND', 'Ireland', 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP',
|
||||
'Mexico', 'MX', 'Netherlands', 'NL', 'NewZealand', 'NZ',
|
||||
'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
|
||||
|
||||
@@ -107,7 +107,7 @@ class XiaomiBinarySensor(XiaomiDevice, BinarySensorDevice):
|
||||
|
||||
def update(self):
|
||||
"""Update the sensor state."""
|
||||
_LOGGER.debug('Updating xiaomi sensor by polling')
|
||||
_LOGGER.debug('Updating xiaomi sensor (%s) by polling', self._sid)
|
||||
self._get_from_hub(self._sid)
|
||||
|
||||
|
||||
@@ -178,7 +178,28 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def parse_data(self, data, raw_data):
|
||||
"""Parse data sent by gateway."""
|
||||
"""Parse data sent by gateway.
|
||||
|
||||
Polling (proto v1, firmware version 1.4.1_159.0143)
|
||||
|
||||
>> { "cmd":"read","sid":"158..."}
|
||||
<< {'model': 'motion', 'sid': '158...', 'short_id': 26331,
|
||||
'cmd': 'read_ack', 'data': '{"voltage":3005}'}
|
||||
|
||||
Multicast messages (proto v1, firmware version 1.4.1_159.0143)
|
||||
|
||||
<< {'model': 'motion', 'sid': '158...', 'short_id': 26331,
|
||||
'cmd': 'report', 'data': '{"status":"motion"}'}
|
||||
<< {'model': 'motion', 'sid': '158...', 'short_id': 26331,
|
||||
'cmd': 'report', 'data': '{"no_motion":"120"}'}
|
||||
<< {'model': 'motion', 'sid': '158...', 'short_id': 26331,
|
||||
'cmd': 'report', 'data': '{"no_motion":"180"}'}
|
||||
<< {'model': 'motion', 'sid': '158...', 'short_id': 26331,
|
||||
'cmd': 'report', 'data': '{"no_motion":"300"}'}
|
||||
<< {'model': 'motion', 'sid': '158...', 'short_id': 26331,
|
||||
'cmd': 'heartbeat', 'data': '{"voltage":3005}'}
|
||||
|
||||
"""
|
||||
if raw_data['cmd'] == 'heartbeat':
|
||||
_LOGGER.debug(
|
||||
'Skipping heartbeat of the motion sensor. '
|
||||
@@ -187,8 +208,7 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
|
||||
'11631#issuecomment-357507744).')
|
||||
return
|
||||
|
||||
self._should_poll = False
|
||||
if NO_MOTION in data: # handle push from the hub
|
||||
if NO_MOTION in data:
|
||||
self._no_motion_since = data[NO_MOTION]
|
||||
self._state = False
|
||||
return True
|
||||
@@ -203,26 +223,20 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
|
||||
self._unsub_set_no_motion()
|
||||
self._unsub_set_no_motion = async_call_later(
|
||||
self._hass,
|
||||
180,
|
||||
120,
|
||||
self._async_set_no_motion
|
||||
)
|
||||
else:
|
||||
self._should_poll = True
|
||||
if self.entity_id is not None:
|
||||
self._hass.bus.fire('xiaomi_aqara.motion', {
|
||||
'entity_id': self.entity_id
|
||||
})
|
||||
|
||||
if self.entity_id is not None:
|
||||
self._hass.bus.fire('xiaomi_aqara.motion', {
|
||||
'entity_id': self.entity_id
|
||||
})
|
||||
|
||||
self._no_motion_since = 0
|
||||
if self._state:
|
||||
return False
|
||||
self._state = True
|
||||
return True
|
||||
if value == NO_MOTION:
|
||||
if not self._state:
|
||||
return False
|
||||
self._state = False
|
||||
return True
|
||||
|
||||
|
||||
class XiaomiDoorSensor(XiaomiBinarySensor):
|
||||
@@ -409,10 +423,14 @@ class XiaomiButton(XiaomiBinarySensor):
|
||||
click_type = 'double'
|
||||
elif value == 'both_click':
|
||||
click_type = 'both'
|
||||
elif value == 'double_both_click':
|
||||
click_type = 'double_both'
|
||||
elif value == 'shake':
|
||||
click_type = 'shake'
|
||||
elif value in ['long_click', 'long_both_click']:
|
||||
return False
|
||||
elif value == 'long_click':
|
||||
click_type = 'long'
|
||||
elif value == 'long_both_click':
|
||||
click_type = 'long_both'
|
||||
else:
|
||||
_LOGGER.warning("Unsupported click_type detected: %s", value)
|
||||
return False
|
||||
@@ -465,4 +483,12 @@ class XiaomiCube(XiaomiBinarySensor):
|
||||
})
|
||||
self._last_action = 'rotate'
|
||||
|
||||
if 'rotate_degree' in data:
|
||||
self._hass.bus.fire('xiaomi_aqara.cube_action', {
|
||||
'entity_id': self.entity_id,
|
||||
'action_type': 'rotate',
|
||||
'action_value': float(data['rotate_degree'].replace(",", "."))
|
||||
})
|
||||
self._last_action = 'rotate'
|
||||
|
||||
return True
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
|
||||
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
|
||||
|
||||
REQUIREMENTS = ['blinkpy==0.11.0']
|
||||
REQUIREMENTS = ['blinkpy==0.11.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ FALLBACK_STREAM_INTERVAL = 1 # seconds
|
||||
MIN_STREAM_INTERVAL = 0.5 # seconds
|
||||
|
||||
CAMERA_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
})
|
||||
|
||||
CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({
|
||||
|
||||
@@ -7,7 +7,7 @@ https://home-assistant.io/components/camera.axis/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.camera.mjpeg import (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera, filter_urllib3_logging)
|
||||
from homeassistant.const import (
|
||||
CONF_AUTHENTICATION, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT,
|
||||
CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION)
|
||||
@@ -29,6 +29,8 @@ def _get_image_url(host, port, mode):
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Axis camera."""
|
||||
filter_urllib3_logging()
|
||||
|
||||
camera_config = {
|
||||
CONF_NAME: discovery_info[CONF_NAME],
|
||||
CONF_USERNAME: discovery_info[CONF_USERNAME],
|
||||
|
||||
@@ -16,7 +16,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
|
||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, CONF_VERIFY_SSL)
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_aiohttp_proxy_web)
|
||||
@@ -29,6 +29,7 @@ CONF_STILL_IMAGE_URL = 'still_image_url'
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
|
||||
DEFAULT_NAME = 'Mjpeg Camera'
|
||||
DEFAULT_VERIFY_SSL = True
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_MJPEG_URL): cv.url,
|
||||
@@ -38,13 +39,22 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up a MJPEG IP Camera."""
|
||||
# Filter header errors from urllib3 due to a urllib3 bug
|
||||
filter_urllib3_logging()
|
||||
|
||||
if discovery_info:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
async_add_entities([MjpegCamera(config)])
|
||||
|
||||
|
||||
def filter_urllib3_logging():
|
||||
"""Filter header errors from urllib3 due to a urllib3 bug."""
|
||||
urllib3_logger = logging.getLogger("urllib3.connectionpool")
|
||||
if not any(isinstance(x, NoHeaderErrorFilter)
|
||||
for x in urllib3_logger.filters):
|
||||
@@ -52,10 +62,6 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
NoHeaderErrorFilter()
|
||||
)
|
||||
|
||||
if discovery_info:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
async_add_entities([MjpegCamera(config)])
|
||||
|
||||
|
||||
def extract_image_from_mjpeg(stream):
|
||||
"""Take in a MJPEG stream object, return the jpg from it."""
|
||||
@@ -95,6 +101,7 @@ class MjpegCamera(Camera):
|
||||
self._auth = aiohttp.BasicAuth(
|
||||
self._username, password=self._password
|
||||
)
|
||||
self._verify_ssl = device_info.get(CONF_VERIFY_SSL)
|
||||
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
@@ -105,7 +112,10 @@ class MjpegCamera(Camera):
|
||||
self.camera_image)
|
||||
return image
|
||||
|
||||
websession = async_get_clientsession(self.hass)
|
||||
websession = async_get_clientsession(
|
||||
self.hass,
|
||||
verify_ssl=self._verify_ssl
|
||||
)
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
response = await websession.get(
|
||||
@@ -128,7 +138,12 @@ class MjpegCamera(Camera):
|
||||
else:
|
||||
auth = HTTPBasicAuth(self._username, self._password)
|
||||
req = requests.get(
|
||||
self._mjpeg_url, auth=auth, stream=True, timeout=10)
|
||||
self._mjpeg_url,
|
||||
auth=auth,
|
||||
stream=True,
|
||||
timeout=10,
|
||||
verify=self._verify_ssl
|
||||
)
|
||||
else:
|
||||
req = requests.get(self._mjpeg_url, stream=True, timeout=10)
|
||||
|
||||
@@ -144,7 +159,10 @@ class MjpegCamera(Camera):
|
||||
return await super().handle_async_mjpeg_stream(request)
|
||||
|
||||
# connect to stream
|
||||
websession = async_get_clientsession(self.hass)
|
||||
websession = async_get_clientsession(
|
||||
self.hass,
|
||||
verify_ssl=self._verify_ssl
|
||||
)
|
||||
stream_coro = websession.get(self._mjpeg_url, auth=self._auth)
|
||||
|
||||
return await async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
|
||||
@@ -7,6 +7,7 @@ https://www.home-assistant.io/components/camera.proxy/
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
|
||||
@@ -18,7 +19,7 @@ from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.camera import async_get_still_stream
|
||||
|
||||
REQUIREMENTS = ['pillow==5.3.0']
|
||||
REQUIREMENTS = ['pillow==5.4.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -206,7 +207,7 @@ class ProxyCamera(Camera):
|
||||
self._cache_images = bool(
|
||||
config.get(CONF_IMAGE_REFRESH_RATE)
|
||||
or config.get(CONF_CACHE_IMAGES))
|
||||
self._last_image_time = 0
|
||||
self._last_image_time = dt_util.utc_from_timestamp(0)
|
||||
self._last_image = None
|
||||
self._headers = (
|
||||
{HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password}
|
||||
@@ -223,7 +224,8 @@ class ProxyCamera(Camera):
|
||||
now = dt_util.utcnow()
|
||||
|
||||
if (self._image_refresh_rate and
|
||||
now < self._last_image_time + self._image_refresh_rate):
|
||||
now < self._last_image_time +
|
||||
timedelta(seconds=self._image_refresh_rate)):
|
||||
return self._last_image
|
||||
|
||||
self._last_image_time = now
|
||||
|
||||
@@ -8,6 +8,11 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.skybell import (
|
||||
@@ -19,14 +24,33 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=90)
|
||||
|
||||
IMAGE_AVATAR = 'avatar'
|
||||
IMAGE_ACTIVITY = 'activity'
|
||||
|
||||
CONF_ACTIVITY_NAME = 'activity_name'
|
||||
CONF_AVATAR_NAME = 'avatar_name'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=[IMAGE_AVATAR]):
|
||||
vol.All(cv.ensure_list, [vol.In([IMAGE_AVATAR, IMAGE_ACTIVITY])]),
|
||||
vol.Optional(CONF_ACTIVITY_NAME): cv.string,
|
||||
vol.Optional(CONF_AVATAR_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the platform for a Skybell device."""
|
||||
cond = config[CONF_MONITORED_CONDITIONS]
|
||||
names = {}
|
||||
names[IMAGE_ACTIVITY] = config.get(CONF_ACTIVITY_NAME)
|
||||
names[IMAGE_AVATAR] = config.get(CONF_AVATAR_NAME)
|
||||
skybell = hass.data.get(SKYBELL_DOMAIN)
|
||||
|
||||
sensors = []
|
||||
for device in skybell.get_devices():
|
||||
sensors.append(SkybellCamera(device))
|
||||
for camera_type in cond:
|
||||
sensors.append(SkybellCamera(device, camera_type,
|
||||
names.get(camera_type)))
|
||||
|
||||
add_entities(sensors, True)
|
||||
|
||||
@@ -34,11 +58,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
class SkybellCamera(SkybellDevice, Camera):
|
||||
"""A camera implementation for Skybell devices."""
|
||||
|
||||
def __init__(self, device):
|
||||
def __init__(self, device, camera_type, name=None):
|
||||
"""Initialize a camera for a Skybell device."""
|
||||
self._type = camera_type
|
||||
SkybellDevice.__init__(self, device)
|
||||
Camera.__init__(self)
|
||||
self._name = self._device.name
|
||||
if name is not None:
|
||||
self._name = "{} {}".format(self._device.name, name)
|
||||
else:
|
||||
self._name = self._device.name
|
||||
self._url = None
|
||||
self._response = None
|
||||
|
||||
@@ -47,12 +75,19 @@ class SkybellCamera(SkybellDevice, Camera):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def image_url(self):
|
||||
"""Get the camera image url based on type."""
|
||||
if self._type == IMAGE_ACTIVITY:
|
||||
return self._device.activity_image
|
||||
return self._device.image
|
||||
|
||||
def camera_image(self):
|
||||
"""Get the latest camera image."""
|
||||
super().update()
|
||||
|
||||
if self._url != self._device.image:
|
||||
self._url = self._device.image
|
||||
if self._url != self.image_url:
|
||||
self._url = self.image_url
|
||||
|
||||
try:
|
||||
self._response = requests.get(
|
||||
|
||||
@@ -107,7 +107,7 @@ class XiaomiCamera(Camera):
|
||||
_LOGGER.warning("There don't appear to be any folders")
|
||||
return False
|
||||
|
||||
first_dir = dirs[-1]
|
||||
first_dir = latest_dir = dirs[-1]
|
||||
try:
|
||||
ftp.cwd(first_dir)
|
||||
except error_perm as exc:
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
|
||||
REQUIREMENTS = ['aioftp==0.10.1']
|
||||
REQUIREMENTS = ['aioftp==0.12.0']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ https://home-assistant.io/components/camera.zoneminder/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.const import CONF_NAME, CONF_VERIFY_SSL
|
||||
from homeassistant.components.camera.mjpeg import (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera, filter_urllib3_logging)
|
||||
from homeassistant.components.zoneminder import DOMAIN as ZONEMINDER_DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -18,32 +18,36 @@ DEPENDENCIES = ['zoneminder']
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the ZoneMinder cameras."""
|
||||
zm_client = hass.data[ZONEMINDER_DOMAIN]
|
||||
|
||||
monitors = zm_client.get_monitors()
|
||||
if not monitors:
|
||||
_LOGGER.warning("Could not fetch monitors from ZoneMinder")
|
||||
return
|
||||
|
||||
filter_urllib3_logging()
|
||||
cameras = []
|
||||
for monitor in monitors:
|
||||
_LOGGER.info("Initializing camera %s", monitor.id)
|
||||
cameras.append(ZoneMinderCamera(monitor))
|
||||
for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
|
||||
monitors = zm_client.get_monitors()
|
||||
if not monitors:
|
||||
_LOGGER.warning(
|
||||
"Could not fetch monitors from ZoneMinder host: %s"
|
||||
)
|
||||
return
|
||||
|
||||
for monitor in monitors:
|
||||
_LOGGER.info("Initializing camera %s", monitor.id)
|
||||
cameras.append(ZoneMinderCamera(monitor, zm_client.verify_ssl))
|
||||
add_entities(cameras)
|
||||
|
||||
|
||||
class ZoneMinderCamera(MjpegCamera):
|
||||
"""Representation of a ZoneMinder Monitor Stream."""
|
||||
|
||||
def __init__(self, monitor):
|
||||
def __init__(self, monitor, verify_ssl):
|
||||
"""Initialize as a subclass of MjpegCamera."""
|
||||
device_info = {
|
||||
CONF_NAME: monitor.name,
|
||||
CONF_MJPEG_URL: monitor.mjpeg_image_url,
|
||||
CONF_STILL_IMAGE_URL: monitor.still_image_url
|
||||
CONF_STILL_IMAGE_URL: monitor.still_image_url,
|
||||
CONF_VERIFY_SSL: verify_ssl
|
||||
}
|
||||
super().__init__(device_info)
|
||||
self._is_recording = None
|
||||
self._is_available = None
|
||||
self._monitor = monitor
|
||||
|
||||
@property
|
||||
@@ -55,8 +59,14 @@ class ZoneMinderCamera(MjpegCamera):
|
||||
"""Update our recording state from the ZM API."""
|
||||
_LOGGER.debug("Updating camera state for monitor %i", self._monitor.id)
|
||||
self._is_recording = self._monitor.is_recording
|
||||
self._is_available = self._monitor.is_available
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
"""Return whether the monitor is in alarm mode."""
|
||||
return self._is_recording
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._is_available
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Voleu configurar Google Cast?",
|
||||
"description": "Vols configurar Google Cast?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
|
||||
10
homeassistant/components/cast/.translations/et.json
Normal file
10
homeassistant/components/cast/.translations/et.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"title": ""
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Nem tal\u00e1lhat\u00f3k Google Cast eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton."
|
||||
"no_devices_found": "Nem tal\u00e1lhat\u00f3k Google Cast eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton.",
|
||||
"single_instance_allowed": "Csak egyetlen Google Cast konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
|
||||
@@ -92,15 +92,15 @@ CONVERTIBLE_ATTRIBUTE = [
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ON_OFF_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
})
|
||||
|
||||
SET_AWAY_MODE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
vol.Required(ATTR_AWAY_MODE): cv.boolean,
|
||||
})
|
||||
SET_AUX_HEAT_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
vol.Required(ATTR_AUX_HEAT): cv.boolean,
|
||||
})
|
||||
SET_TEMPERATURE_SCHEMA = vol.Schema(vol.All(
|
||||
@@ -110,28 +110,28 @@ SET_TEMPERATURE_SCHEMA = vol.Schema(vol.All(
|
||||
vol.Exclusive(ATTR_TEMPERATURE, 'temperature'): vol.Coerce(float),
|
||||
vol.Inclusive(ATTR_TARGET_TEMP_HIGH, 'temperature'): vol.Coerce(float),
|
||||
vol.Inclusive(ATTR_TARGET_TEMP_LOW, 'temperature'): vol.Coerce(float),
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
vol.Optional(ATTR_OPERATION_MODE): cv.string,
|
||||
}
|
||||
))
|
||||
SET_FAN_MODE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
vol.Required(ATTR_FAN_MODE): cv.string,
|
||||
})
|
||||
SET_HOLD_MODE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
vol.Required(ATTR_HOLD_MODE): cv.string,
|
||||
})
|
||||
SET_OPERATION_MODE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
vol.Required(ATTR_OPERATION_MODE): cv.string,
|
||||
})
|
||||
SET_HUMIDITY_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
vol.Required(ATTR_HUMIDITY): vol.Coerce(float),
|
||||
})
|
||||
SET_SWING_MODE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
vol.Required(ATTR_SWING_MODE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -15,14 +15,13 @@ from homeassistant.components.climate import (
|
||||
STATE_FAN_ONLY, STATE_HEAT, STATE_OFF, SUPPORT_FAN_MODE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE,
|
||||
ClimateDevice)
|
||||
from homeassistant.components.daikin import (
|
||||
ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, ATTR_TARGET_TEMPERATURE,
|
||||
daikin_api_setup)
|
||||
from homeassistant.components.daikin import DOMAIN as DAIKIN_DOMAIN
|
||||
from homeassistant.components.daikin.const import (
|
||||
ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, ATTR_TARGET_TEMPERATURE)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, TEMP_CELSIUS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pydaikin==0.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -60,18 +59,18 @@ HA_ATTR_TO_DAIKIN = {
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Daikin HVAC platform."""
|
||||
if discovery_info is not None:
|
||||
host = discovery_info.get('ip')
|
||||
name = None
|
||||
_LOGGER.debug("Discovered a Daikin AC on %s", host)
|
||||
else:
|
||||
host = config.get(CONF_HOST)
|
||||
name = config.get(CONF_NAME)
|
||||
_LOGGER.debug("Added Daikin AC on %s", host)
|
||||
"""Old way of setting up the Daikin HVAC platform.
|
||||
|
||||
api = daikin_api_setup(hass, host, name)
|
||||
add_entities([DaikinClimate(api)], True)
|
||||
Can only be called when a user accidentally mentions the platform in their
|
||||
config. But even in that case it would have been ignored.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up Daikin climate based on config_entry."""
|
||||
daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id)
|
||||
async_add_entities([DaikinClimate(daikin_api)])
|
||||
|
||||
|
||||
class DaikinClimate(ClimateDevice):
|
||||
@@ -266,3 +265,8 @@ class DaikinClimate(ClimateDevice):
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._api.update()
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return a device description for device registry."""
|
||||
return self._api.device_info
|
||||
|
||||
@@ -9,7 +9,8 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
STATE_ON, STATE_OFF, STATE_AUTO, PLATFORM_SCHEMA, ClimateDevice,
|
||||
STATE_ON, STATE_OFF, STATE_HEAT, STATE_MANUAL, STATE_ECO, PLATFORM_SCHEMA,
|
||||
ClimateDevice,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE,
|
||||
SUPPORT_ON_OFF)
|
||||
from homeassistant.const import (
|
||||
@@ -21,8 +22,6 @@ REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.45']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_BOOST = 'boost'
|
||||
STATE_AWAY = 'away'
|
||||
STATE_MANUAL = 'manual'
|
||||
|
||||
ATTR_STATE_WINDOW_OPEN = 'window_open'
|
||||
ATTR_STATE_VALVE = 'valve'
|
||||
@@ -65,10 +64,10 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
self.modes = {
|
||||
eq3.Mode.Open: STATE_ON,
|
||||
eq3.Mode.Closed: STATE_OFF,
|
||||
eq3.Mode.Auto: STATE_AUTO,
|
||||
eq3.Mode.Auto: STATE_HEAT,
|
||||
eq3.Mode.Manual: STATE_MANUAL,
|
||||
eq3.Mode.Boost: STATE_BOOST,
|
||||
eq3.Mode.Away: STATE_AWAY,
|
||||
eq3.Mode.Away: STATE_ECO,
|
||||
}
|
||||
|
||||
self.reverse_modes = {v: k for k, v in self.modes.items()}
|
||||
@@ -140,20 +139,20 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Away mode off turns to AUTO mode."""
|
||||
self.set_operation_mode(STATE_AUTO)
|
||||
self.set_operation_mode(STATE_HEAT)
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Set away mode on."""
|
||||
self.set_operation_mode(STATE_AWAY)
|
||||
self.set_operation_mode(STATE_ECO)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return if we are away."""
|
||||
return self.current_operation == STATE_AWAY
|
||||
return self.current_operation == STATE_ECO
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn device on."""
|
||||
self.set_operation_mode(STATE_AUTO)
|
||||
self.set_operation_mode(STATE_HEAT)
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn device off."""
|
||||
|
||||
@@ -8,7 +8,7 @@ from homeassistant.components.climate import (
|
||||
ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON,
|
||||
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
|
||||
from homeassistant.components.hive import DATA_HIVE
|
||||
from homeassistant.components.hive import DATA_HIVE, DOMAIN
|
||||
|
||||
DEPENDENCIES = ['hive']
|
||||
HIVE_TO_HASS_STATE = {'SCHEDULE': STATE_AUTO, 'MANUAL': STATE_HEAT,
|
||||
@@ -44,6 +44,7 @@ class HiveClimateEntity(ClimateDevice):
|
||||
self.attributes = {}
|
||||
self.data_updatesource = '{}.{}'.format(self.device_type,
|
||||
self.node_id)
|
||||
self._unique_id = '{}-{}'.format(self.node_id, self.device_type)
|
||||
|
||||
if self.device_type == "Heating":
|
||||
self.modes = [STATE_AUTO, STATE_HEAT, STATE_OFF]
|
||||
@@ -52,6 +53,21 @@ class HiveClimateEntity(ClimateDevice):
|
||||
|
||||
self.session.entities.append(self)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique ID of entity."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device information."""
|
||||
return {
|
||||
'identifiers': {
|
||||
(DOMAIN, self.unique_id)
|
||||
},
|
||||
'name': self.name
|
||||
}
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
|
||||
@@ -50,23 +50,23 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice):
|
||||
def update_characteristics(self, characteristics):
|
||||
"""Synchronise device state with Home Assistant."""
|
||||
# pylint: disable=import-error
|
||||
from homekit import CharacteristicsTypes as ctypes
|
||||
from homekit.models.characteristics import CharacteristicsTypes
|
||||
|
||||
for characteristic in characteristics:
|
||||
ctype = characteristic['type']
|
||||
if ctype == ctypes.HEATING_COOLING_CURRENT:
|
||||
if ctype == CharacteristicsTypes.HEATING_COOLING_CURRENT:
|
||||
self._state = MODE_HOMEKIT_TO_HASS.get(
|
||||
characteristic['value'])
|
||||
if ctype == ctypes.HEATING_COOLING_TARGET:
|
||||
if ctype == CharacteristicsTypes.HEATING_COOLING_TARGET:
|
||||
self._chars['target_mode'] = characteristic['iid']
|
||||
self._features |= SUPPORT_OPERATION_MODE
|
||||
self._current_mode = MODE_HOMEKIT_TO_HASS.get(
|
||||
characteristic['value'])
|
||||
self._valid_modes = [MODE_HOMEKIT_TO_HASS.get(
|
||||
mode) for mode in characteristic['valid-values']]
|
||||
elif ctype == ctypes.TEMPERATURE_CURRENT:
|
||||
elif ctype == CharacteristicsTypes.TEMPERATURE_CURRENT:
|
||||
self._current_temp = characteristic['value']
|
||||
elif ctype == ctypes.TEMPERATURE_TARGET:
|
||||
elif ctype == CharacteristicsTypes.TEMPERATURE_TARGET:
|
||||
self._chars['target_temp'] = characteristic['iid']
|
||||
self._features |= SUPPORT_TARGET_TEMPERATURE
|
||||
self._target_temp = characteristic['value']
|
||||
|
||||
@@ -6,14 +6,17 @@ https://home-assistant.io/components/climate.knx/
|
||||
"""
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
PLATFORM_SCHEMA, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE,
|
||||
ClimateDevice)
|
||||
from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX
|
||||
from homeassistant.const import ATTR_TEMPERATURE, CONF_NAME, TEMP_CELSIUS
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.climate import (
|
||||
PLATFORM_SCHEMA, SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE, STATE_HEAT,
|
||||
STATE_IDLE, STATE_MANUAL, STATE_DRY,
|
||||
STATE_FAN_ONLY, STATE_ECO, ClimateDevice)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_NAME, TEMP_CELSIUS)
|
||||
from homeassistant.core import callback
|
||||
|
||||
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES
|
||||
|
||||
CONF_SETPOINT_SHIFT_ADDRESS = 'setpoint_shift_address'
|
||||
CONF_SETPOINT_SHIFT_STATE_ADDRESS = 'setpoint_shift_state_address'
|
||||
@@ -26,10 +29,17 @@ CONF_OPERATION_MODE_ADDRESS = 'operation_mode_address'
|
||||
CONF_OPERATION_MODE_STATE_ADDRESS = 'operation_mode_state_address'
|
||||
CONF_CONTROLLER_STATUS_ADDRESS = 'controller_status_address'
|
||||
CONF_CONTROLLER_STATUS_STATE_ADDRESS = 'controller_status_state_address'
|
||||
CONF_CONTROLLER_MODE_ADDRESS = 'controller_mode_address'
|
||||
CONF_CONTROLLER_MODE_STATE_ADDRESS = 'controller_mode_state_address'
|
||||
CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS = \
|
||||
'operation_mode_frost_protection_address'
|
||||
CONF_OPERATION_MODE_NIGHT_ADDRESS = 'operation_mode_night_address'
|
||||
CONF_OPERATION_MODE_COMFORT_ADDRESS = 'operation_mode_comfort_address'
|
||||
CONF_OPERATION_MODES = 'operation_modes'
|
||||
CONF_ON_OFF_ADDRESS = 'on_off_address'
|
||||
CONF_ON_OFF_STATE_ADDRESS = 'on_off_state_address'
|
||||
CONF_MIN_TEMP = 'min_temp'
|
||||
CONF_MAX_TEMP = 'max_temp'
|
||||
|
||||
DEFAULT_NAME = 'KNX Climate'
|
||||
DEFAULT_SETPOINT_SHIFT_STEP = 0.5
|
||||
@@ -37,6 +47,21 @@ DEFAULT_SETPOINT_SHIFT_MAX = 6
|
||||
DEFAULT_SETPOINT_SHIFT_MIN = -6
|
||||
DEPENDENCIES = ['knx']
|
||||
|
||||
# Map KNX operation modes to HA modes. This list might not be full.
|
||||
OPERATION_MODES = {
|
||||
# Map DPT 201.100 HVAC operating modes
|
||||
"Frost Protection": STATE_MANUAL,
|
||||
"Night": STATE_IDLE,
|
||||
"Standby": STATE_ECO,
|
||||
"Comfort": STATE_HEAT,
|
||||
# Map DPT 201.104 HVAC control modes
|
||||
"Fan only": STATE_FAN_ONLY,
|
||||
"Dehumidification": STATE_DRY
|
||||
}
|
||||
|
||||
OPERATION_MODES_INV = dict((
|
||||
reversed(item) for item in OPERATION_MODES.items()))
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string,
|
||||
@@ -54,9 +79,17 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_CONTROLLER_MODE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_CONTROLLER_MODE_STATE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_ON_OFF_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_ON_OFF_STATE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_OPERATION_MODES): vol.All(cv.ensure_list,
|
||||
[vol.In(OPERATION_MODES)]),
|
||||
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
|
||||
})
|
||||
|
||||
|
||||
@@ -84,6 +117,30 @@ def async_add_entities_config(hass, config, async_add_entities):
|
||||
"""Set up climate for KNX platform configured within platform."""
|
||||
import xknx
|
||||
|
||||
climate_mode = xknx.devices.ClimateMode(
|
||||
hass.data[DATA_KNX].xknx,
|
||||
name=config.get(CONF_NAME) + " Mode",
|
||||
group_address_operation_mode=config.get(CONF_OPERATION_MODE_ADDRESS),
|
||||
group_address_operation_mode_state=config.get(
|
||||
CONF_OPERATION_MODE_STATE_ADDRESS),
|
||||
group_address_controller_status=config.get(
|
||||
CONF_CONTROLLER_STATUS_ADDRESS),
|
||||
group_address_controller_status_state=config.get(
|
||||
CONF_CONTROLLER_STATUS_STATE_ADDRESS),
|
||||
group_address_controller_mode=config.get(
|
||||
CONF_CONTROLLER_MODE_ADDRESS),
|
||||
group_address_controller_mode_state=config.get(
|
||||
CONF_CONTROLLER_MODE_STATE_ADDRESS),
|
||||
group_address_operation_mode_protection=config.get(
|
||||
CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS),
|
||||
group_address_operation_mode_night=config.get(
|
||||
CONF_OPERATION_MODE_NIGHT_ADDRESS),
|
||||
group_address_operation_mode_comfort=config.get(
|
||||
CONF_OPERATION_MODE_COMFORT_ADDRESS),
|
||||
operation_modes=config.get(
|
||||
CONF_OPERATION_MODES))
|
||||
hass.data[DATA_KNX].xknx.devices.add(climate_mode)
|
||||
|
||||
climate = xknx.devices.Climate(
|
||||
hass.data[DATA_KNX].xknx,
|
||||
name=config.get(CONF_NAME),
|
||||
@@ -96,20 +153,15 @@ def async_add_entities_config(hass, config, async_add_entities):
|
||||
setpoint_shift_step=config.get(CONF_SETPOINT_SHIFT_STEP),
|
||||
setpoint_shift_max=config.get(CONF_SETPOINT_SHIFT_MAX),
|
||||
setpoint_shift_min=config.get(CONF_SETPOINT_SHIFT_MIN),
|
||||
group_address_operation_mode=config.get(CONF_OPERATION_MODE_ADDRESS),
|
||||
group_address_operation_mode_state=config.get(
|
||||
CONF_OPERATION_MODE_STATE_ADDRESS),
|
||||
group_address_controller_status=config.get(
|
||||
CONF_CONTROLLER_STATUS_ADDRESS),
|
||||
group_address_controller_status_state=config.get(
|
||||
CONF_CONTROLLER_STATUS_STATE_ADDRESS),
|
||||
group_address_operation_mode_protection=config.get(
|
||||
CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS),
|
||||
group_address_operation_mode_night=config.get(
|
||||
CONF_OPERATION_MODE_NIGHT_ADDRESS),
|
||||
group_address_operation_mode_comfort=config.get(
|
||||
CONF_OPERATION_MODE_COMFORT_ADDRESS))
|
||||
group_address_on_off=config.get(
|
||||
CONF_ON_OFF_ADDRESS),
|
||||
group_address_on_off_state=config.get(
|
||||
CONF_ON_OFF_STATE_ADDRESS),
|
||||
min_temp=config.get(CONF_MIN_TEMP),
|
||||
max_temp=config.get(CONF_MAX_TEMP),
|
||||
mode=climate_mode)
|
||||
hass.data[DATA_KNX].xknx.devices.add(climate)
|
||||
|
||||
async_add_entities([KNXClimate(climate)])
|
||||
|
||||
|
||||
@@ -119,26 +171,25 @@ class KNXClimate(ClimateDevice):
|
||||
def __init__(self, device):
|
||||
"""Initialize of a KNX climate device."""
|
||||
self.device = device
|
||||
self._unit_of_measurement = TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
support = SUPPORT_TARGET_TEMPERATURE
|
||||
if self.device.supports_operation_mode:
|
||||
if self.device.mode.supports_operation_mode:
|
||||
support |= SUPPORT_OPERATION_MODE
|
||||
if self.device.supports_on_off:
|
||||
support |= SUPPORT_ON_OFF
|
||||
return support
|
||||
|
||||
def async_register_callbacks(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
async def after_update_callback(device):
|
||||
"""Call after device was updated."""
|
||||
await self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Store register state change callback."""
|
||||
self.async_register_callbacks()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the KNX device."""
|
||||
@@ -157,7 +208,7 @@ class KNXClimate(ClimateDevice):
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
@@ -195,20 +246,37 @@ class KNXClimate(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if self.device.supports_operation_mode:
|
||||
return self.device.operation_mode.value
|
||||
if self.device.mode.supports_operation_mode:
|
||||
return OPERATION_MODES.get(self.device.mode.operation_mode.value)
|
||||
return None
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return [operation_mode.value for
|
||||
return [OPERATION_MODES.get(operation_mode.value) for
|
||||
operation_mode in
|
||||
self.device.get_supported_operation_modes()]
|
||||
self.device.mode.operation_modes]
|
||||
|
||||
async def async_set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if self.device.supports_operation_mode:
|
||||
if self.device.mode.supports_operation_mode:
|
||||
from xknx.knx import HVACOperationMode
|
||||
knx_operation_mode = HVACOperationMode(operation_mode)
|
||||
await self.device.set_operation_mode(knx_operation_mode)
|
||||
knx_operation_mode = HVACOperationMode(
|
||||
OPERATION_MODES_INV.get(operation_mode))
|
||||
await self.device.mode.set_operation_mode(knx_operation_mode)
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the device is on."""
|
||||
if self.device.supports_on_off:
|
||||
return self.device.is_on
|
||||
return None
|
||||
|
||||
async def async_turn_on(self):
|
||||
"""Turn on."""
|
||||
await self.device.turn_on()
|
||||
|
||||
async def async_turn_off(self):
|
||||
"""Turn off."""
|
||||
await self.device.turn_off()
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
REQUIREMENTS = ['millheater==0.2.9']
|
||||
REQUIREMENTS = ['millheater==0.3.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.const import (
|
||||
CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, PRECISION_HALVES)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['radiotherm==1.4.1']
|
||||
REQUIREMENTS = ['radiotherm==2.0.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -235,13 +235,15 @@ class RadioThermostat(ClimateDevice):
|
||||
self._name = self.device.name['raw']
|
||||
|
||||
# Request the current state from the thermostat.
|
||||
data = self.device.tstat['raw']
|
||||
import radiotherm
|
||||
try:
|
||||
data = self.device.tstat['raw']
|
||||
except radiotherm.validate.RadiothermTstatError:
|
||||
_LOGGER.error('%s (%s) was busy (invalid value returned)',
|
||||
self._name, self.device.host)
|
||||
return
|
||||
|
||||
current_temp = data['temp']
|
||||
if current_temp == -1:
|
||||
_LOGGER.error('%s (%s) was busy (temp == -1)', self._name,
|
||||
self.device.host)
|
||||
return
|
||||
|
||||
# Map thermostat values into various STATE_ flags.
|
||||
self._current_temperature = current_temp
|
||||
|
||||
@@ -19,7 +19,8 @@ from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_FAN_MODE, SUPPORT_SWING_MODE,
|
||||
SUPPORT_ON_OFF)
|
||||
SUPPORT_ON_OFF, STATE_HEAT, STATE_COOL, STATE_FAN_ONLY, STATE_DRY,
|
||||
STATE_AUTO)
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -57,6 +58,16 @@ FIELD_TO_FLAG = {
|
||||
'on': SUPPORT_ON_OFF,
|
||||
}
|
||||
|
||||
SENSIBO_TO_HA = {
|
||||
"cool": STATE_COOL,
|
||||
"heat": STATE_HEAT,
|
||||
"fan": STATE_FAN_ONLY,
|
||||
"auto": STATE_AUTO,
|
||||
"dry": STATE_DRY
|
||||
}
|
||||
|
||||
HA_TO_SENSIBO = {value: key for key, value in SENSIBO_TO_HA.items()}
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
@@ -129,9 +140,10 @@ class SensiboClimate(ClimateDevice):
|
||||
self._ac_states = data['acState']
|
||||
self._status = data['connectionStatus']['isAlive']
|
||||
capabilities = data['remoteCapabilities']
|
||||
self._operations = sorted(capabilities['modes'].keys())
|
||||
self._current_capabilities = capabilities[
|
||||
'modes'][self.current_operation]
|
||||
self._operations = [SENSIBO_TO_HA[mode] for mode
|
||||
in capabilities['modes']]
|
||||
self._current_capabilities = \
|
||||
capabilities['modes'][self._ac_states['mode']]
|
||||
temperature_unit_key = data.get('temperatureUnit') or \
|
||||
self._ac_states.get('temperatureUnit')
|
||||
if temperature_unit_key:
|
||||
@@ -186,7 +198,7 @@ class SensiboClimate(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self._ac_states['mode']
|
||||
return SENSIBO_TO_HA.get(self._ac_states['mode'])
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
@@ -293,7 +305,8 @@ class SensiboClimate(ClimateDevice):
|
||||
"""Set new target operation mode."""
|
||||
with async_timeout.timeout(TIMEOUT):
|
||||
await self._client.async_set_ac_state_property(
|
||||
self._id, 'mode', operation_mode, self._ac_states)
|
||||
self._id, 'mode', HA_TO_SENSIBO[operation_mode],
|
||||
self._ac_states)
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing operation."""
|
||||
|
||||
@@ -99,6 +99,8 @@ async def async_setup(hass, config):
|
||||
kwargs[CONF_GOOGLE_ACTIONS] = GACTIONS_SCHEMA({})
|
||||
|
||||
kwargs[CONF_ALEXA] = alexa_sh.Config(
|
||||
endpoint=None,
|
||||
async_get_access_token=None,
|
||||
should_expose=alexa_conf[CONF_FILTER],
|
||||
entity_config=alexa_conf.get(CONF_ENTITY_CONFIG),
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import pprint
|
||||
import random
|
||||
import uuid
|
||||
|
||||
from aiohttp import hdrs, client_exceptions, WSMsgType
|
||||
@@ -107,9 +108,11 @@ class CloudIoT:
|
||||
self.tries += 1
|
||||
|
||||
try:
|
||||
# Sleep 2^tries seconds between retries
|
||||
self.retry_task = hass.async_create_task(asyncio.sleep(
|
||||
2**min(9, self.tries), loop=hass.loop))
|
||||
# Sleep 2^tries + 0…tries*3 seconds between retries
|
||||
self.retry_task = hass.async_create_task(
|
||||
asyncio.sleep(2**min(9, self.tries) +
|
||||
random.randint(0, self.tries * 3),
|
||||
loop=hass.loop))
|
||||
yield from self.retry_task
|
||||
self.retry_task = None
|
||||
except asyncio.CancelledError:
|
||||
@@ -313,15 +316,20 @@ def async_handle_google_actions(hass, cloud, payload):
|
||||
|
||||
|
||||
@HANDLERS.register('cloud')
|
||||
@asyncio.coroutine
|
||||
def async_handle_cloud(hass, cloud, payload):
|
||||
async def async_handle_cloud(hass, cloud, payload):
|
||||
"""Handle an incoming IoT message for cloud component."""
|
||||
action = payload['action']
|
||||
|
||||
if action == 'logout':
|
||||
yield from cloud.logout()
|
||||
# Log out of Home Assistant Cloud
|
||||
await cloud.logout()
|
||||
_LOGGER.error("You have been logged out from Home Assistant cloud: %s",
|
||||
payload['reason'])
|
||||
elif action == 'refresh_auth':
|
||||
# Refresh the auth token between now and payload['seconds']
|
||||
hass.helpers.event.async_call_later(
|
||||
random.randint(0, payload['seconds']),
|
||||
lambda now: auth_api.check_token(cloud))
|
||||
else:
|
||||
_LOGGER.warning("Received unknown cloud action: %s", action)
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ async def async_setup(hass):
|
||||
return True
|
||||
|
||||
|
||||
@websocket_api.require_owner
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
async def websocket_list(hass, connection, msg):
|
||||
"""Return a list of users."""
|
||||
@@ -49,7 +49,7 @@ async def websocket_list(hass, connection, msg):
|
||||
websocket_api.result_message(msg['id'], result))
|
||||
|
||||
|
||||
@websocket_api.require_owner
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
async def websocket_delete(hass, connection, msg):
|
||||
"""Delete a user."""
|
||||
@@ -72,7 +72,7 @@ async def websocket_delete(hass, connection, msg):
|
||||
websocket_api.result_message(msg['id']))
|
||||
|
||||
|
||||
@websocket_api.require_owner
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
async def websocket_create(hass, connection, msg):
|
||||
"""Create a user."""
|
||||
|
||||
@@ -3,7 +3,6 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.providers import homeassistant as auth_ha
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.websocket_api.decorators import require_owner
|
||||
|
||||
|
||||
WS_TYPE_CREATE = 'config/auth_provider/homeassistant/create'
|
||||
@@ -54,7 +53,7 @@ def _get_provider(hass):
|
||||
raise RuntimeError('Provider not found')
|
||||
|
||||
|
||||
@require_owner
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
async def websocket_create(hass, connection, msg):
|
||||
"""Create credentials and attach to a user."""
|
||||
@@ -91,7 +90,7 @@ async def websocket_create(hass, connection, msg):
|
||||
connection.send_message(websocket_api.result_message(msg['id']))
|
||||
|
||||
|
||||
@require_owner
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
async def websocket_delete(hass, connection, msg):
|
||||
"""Delete username and related credential."""
|
||||
@@ -123,6 +122,7 @@ async def websocket_delete(hass, connection, msg):
|
||||
websocket_api.result_message(msg['id']))
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
async def websocket_change_password(hass, connection, msg):
|
||||
"""Change user password."""
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Http views to control the config manager."""
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.exceptions import Unauthorized
|
||||
from homeassistant.helpers.data_entry_flow import (
|
||||
FlowManagerIndexView, FlowManagerResourceView)
|
||||
|
||||
@@ -63,6 +65,9 @@ class ConfigManagerEntryResourceView(HomeAssistantView):
|
||||
|
||||
async def delete(self, request, entry_id):
|
||||
"""Delete a config entry."""
|
||||
if not request['hass_user'].is_admin:
|
||||
raise Unauthorized(config_entry_id=entry_id, permission='remove')
|
||||
|
||||
hass = request.app['hass']
|
||||
|
||||
try:
|
||||
@@ -85,12 +90,26 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView):
|
||||
Example of a non-user initiated flow is a discovered Hue hub that
|
||||
requires user interaction to finish setup.
|
||||
"""
|
||||
if not request['hass_user'].is_admin:
|
||||
raise Unauthorized(
|
||||
perm_category=CAT_CONFIG_ENTRIES, permission='add')
|
||||
|
||||
hass = request.app['hass']
|
||||
|
||||
return self.json([
|
||||
flw for flw in hass.config_entries.flow.async_progress()
|
||||
if flw['context']['source'] != config_entries.SOURCE_USER])
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
async def post(self, request):
|
||||
"""Handle a POST request."""
|
||||
if not request['hass_user'].is_admin:
|
||||
raise Unauthorized(
|
||||
perm_category=CAT_CONFIG_ENTRIES, permission='add')
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
return await super().post(request)
|
||||
|
||||
|
||||
class ConfigManagerFlowResourceView(FlowManagerResourceView):
|
||||
"""View to interact with the flow manager."""
|
||||
@@ -98,6 +117,24 @@ class ConfigManagerFlowResourceView(FlowManagerResourceView):
|
||||
url = '/api/config/config_entries/flow/{flow_id}'
|
||||
name = 'api:config:config_entries:flow:resource'
|
||||
|
||||
async def get(self, request, flow_id):
|
||||
"""Get the current state of a data_entry_flow."""
|
||||
if not request['hass_user'].is_admin:
|
||||
raise Unauthorized(
|
||||
perm_category=CAT_CONFIG_ENTRIES, permission='add')
|
||||
|
||||
return await super().get(request, flow_id)
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
async def post(self, request, flow_id):
|
||||
"""Handle a POST request."""
|
||||
if not request['hass_user'].is_admin:
|
||||
raise Unauthorized(
|
||||
perm_category=CAT_CONFIG_ENTRIES, permission='add')
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
return await super().post(request, flow_id)
|
||||
|
||||
|
||||
class ConfigManagerAvailableFlowView(HomeAssistantView):
|
||||
"""View to query available flows."""
|
||||
|
||||
@@ -33,12 +33,12 @@ SERVICE_INCREMENT = 'increment'
|
||||
SERVICE_RESET = 'reset'
|
||||
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
cv.slug: vol.Any({
|
||||
DOMAIN: cv.schema_with_slug_keys(
|
||||
vol.Any({
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL):
|
||||
cv.positive_int,
|
||||
@@ -46,7 +46,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_RESTORE, default=True): cv.boolean,
|
||||
vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int,
|
||||
}, None)
|
||||
})
|
||||
)
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ INTENT_OPEN_COVER = 'HassOpenCover'
|
||||
INTENT_CLOSE_COVER = 'HassCloseCover'
|
||||
|
||||
COVER_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
})
|
||||
|
||||
COVER_SET_COVER_POSITION_SCHEMA = COVER_SERVICE_SCHEMA.extend({
|
||||
|
||||
@@ -27,7 +27,7 @@ COVER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
|
||||
vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA),
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import logging
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, ENTITY_ID_FORMAT, ATTR_POSITION, ATTR_TILT_POSITION)
|
||||
from homeassistant.components.fibaro import (
|
||||
FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice)
|
||||
FIBARO_DEVICES, FibaroDevice)
|
||||
|
||||
DEPENDENCIES = ['fibaro']
|
||||
|
||||
@@ -22,16 +22,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
return
|
||||
|
||||
add_entities(
|
||||
[FibaroCover(device, hass.data[FIBARO_CONTROLLER]) for
|
||||
[FibaroCover(device) for
|
||||
device in hass.data[FIBARO_DEVICES]['cover']], True)
|
||||
|
||||
|
||||
class FibaroCover(FibaroDevice, CoverDevice):
|
||||
"""Representation a Fibaro Cover."""
|
||||
|
||||
def __init__(self, fibaro_device, controller):
|
||||
def __init__(self, fibaro_device):
|
||||
"""Initialize the Vera device."""
|
||||
super().__init__(fibaro_device, controller)
|
||||
super().__init__(fibaro_device)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -47,7 +47,7 @@ COVER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
|
||||
vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA),
|
||||
})
|
||||
|
||||
|
||||
|
||||
305
homeassistant/components/cover/homekit_controller.py
Normal file
305
homeassistant/components/cover/homekit_controller.py
Normal file
@@ -0,0 +1,305 @@
|
||||
"""
|
||||
Support for Homekit Cover.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.homekit_controller/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.homekit_controller import (HomeKitEntity,
|
||||
KNOWN_ACCESSORIES)
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION,
|
||||
SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, SUPPORT_SET_TILT_POSITION,
|
||||
ATTR_POSITION, ATTR_TILT_POSITION)
|
||||
from homeassistant.const import (
|
||||
STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING)
|
||||
|
||||
STATE_STOPPED = 'stopped'
|
||||
|
||||
DEPENDENCIES = ['homekit_controller']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CURRENT_GARAGE_STATE_MAP = {
|
||||
0: STATE_OPEN,
|
||||
1: STATE_CLOSED,
|
||||
2: STATE_OPENING,
|
||||
3: STATE_CLOSING,
|
||||
4: STATE_STOPPED
|
||||
}
|
||||
|
||||
TARGET_GARAGE_STATE_MAP = {
|
||||
STATE_OPEN: 0,
|
||||
STATE_CLOSED: 1,
|
||||
STATE_STOPPED: 2
|
||||
}
|
||||
|
||||
CURRENT_WINDOW_STATE_MAP = {
|
||||
0: STATE_OPENING,
|
||||
1: STATE_CLOSING,
|
||||
2: STATE_STOPPED
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up HomeKit Cover support."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']]
|
||||
|
||||
if discovery_info['device-type'] == 'garage-door-opener':
|
||||
add_entities([HomeKitGarageDoorCover(accessory, discovery_info)],
|
||||
True)
|
||||
else:
|
||||
add_entities([HomeKitWindowCover(accessory, discovery_info)],
|
||||
True)
|
||||
|
||||
|
||||
class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice):
|
||||
"""Representation of a HomeKit Garage Door."""
|
||||
|
||||
def __init__(self, accessory, discovery_info):
|
||||
"""Initialise the Cover."""
|
||||
super().__init__(accessory, discovery_info)
|
||||
self._name = None
|
||||
self._state = None
|
||||
self._obstruction_detected = None
|
||||
self.lock_state = None
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Define this cover as a garage door."""
|
||||
return 'garage'
|
||||
|
||||
def update_characteristics(self, characteristics):
|
||||
"""Synchronise the Cover state with Home Assistant."""
|
||||
# pylint: disable=import-error
|
||||
from homekit.model.characteristics import CharacteristicsTypes
|
||||
|
||||
for characteristic in characteristics:
|
||||
ctype = characteristic['type']
|
||||
ctype = CharacteristicsTypes.get_short(ctype)
|
||||
if ctype == "door-state.current":
|
||||
self._chars['door-state.current'] = \
|
||||
characteristic['iid']
|
||||
self._state = CURRENT_GARAGE_STATE_MAP[characteristic['value']]
|
||||
elif ctype == "door-state.target":
|
||||
self._chars['door-state.target'] = \
|
||||
characteristic['iid']
|
||||
elif ctype == "obstruction-detected":
|
||||
self._chars['obstruction-detected'] = characteristic['iid']
|
||||
self._obstruction_detected = characteristic['value']
|
||||
elif ctype == "name":
|
||||
self._chars['name'] = characteristic['iid']
|
||||
self._name = characteristic['value']
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the cover."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._state is not None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return true if cover is closed, else False."""
|
||||
return self._state == STATE_CLOSED
|
||||
|
||||
@property
|
||||
def is_closing(self):
|
||||
"""Return if the cover is closing or not."""
|
||||
return self._state == STATE_CLOSING
|
||||
|
||||
@property
|
||||
def is_opening(self):
|
||||
"""Return if the cover is opening or not."""
|
||||
return self._state == STATE_OPENING
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Send open command."""
|
||||
self.set_door_state(STATE_OPEN)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Send close command."""
|
||||
self.set_door_state(STATE_CLOSED)
|
||||
|
||||
def set_door_state(self, state):
|
||||
"""Send state command."""
|
||||
characteristics = [{'aid': self._aid,
|
||||
'iid': self._chars['door-state.target'],
|
||||
'value': TARGET_GARAGE_STATE_MAP[state]}]
|
||||
self.put_characteristics(characteristics)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
if self._obstruction_detected is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
'obstruction-detected': self._obstruction_detected,
|
||||
}
|
||||
|
||||
|
||||
class HomeKitWindowCover(HomeKitEntity, CoverDevice):
|
||||
"""Representation of a HomeKit Window or Window Covering."""
|
||||
|
||||
def __init__(self, accessory, discovery_info):
|
||||
"""Initialise the Cover."""
|
||||
super().__init__(accessory, discovery_info)
|
||||
self._name = None
|
||||
self._state = None
|
||||
self._position = None
|
||||
self._tilt_position = None
|
||||
self._hold = None
|
||||
self._obstruction_detected = None
|
||||
self.lock_state = None
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._state is not None
|
||||
|
||||
def update_characteristics(self, characteristics):
|
||||
"""Synchronise the Cover state with Home Assistant."""
|
||||
# pylint: disable=import-error
|
||||
from homekit.model.characteristics import CharacteristicsTypes
|
||||
|
||||
for characteristic in characteristics:
|
||||
ctype = characteristic['type']
|
||||
ctype = CharacteristicsTypes.get_short(ctype)
|
||||
if ctype == "position.state":
|
||||
self._chars['position.state'] = \
|
||||
characteristic['iid']
|
||||
if 'value' in characteristic:
|
||||
self._state = \
|
||||
CURRENT_WINDOW_STATE_MAP[characteristic['value']]
|
||||
elif ctype == "position.current":
|
||||
self._chars['position.current'] = \
|
||||
characteristic['iid']
|
||||
self._position = characteristic['value']
|
||||
elif ctype == "position.target":
|
||||
self._chars['position.target'] = \
|
||||
characteristic['iid']
|
||||
elif ctype == "position.hold":
|
||||
self._chars['position.hold'] = characteristic['iid']
|
||||
if 'value' in characteristic:
|
||||
self._hold = characteristic['value']
|
||||
elif ctype == "vertical-tilt.current":
|
||||
self._chars['vertical-tilt.current'] = characteristic['iid']
|
||||
if characteristic['value'] is not None:
|
||||
self._tilt_position = characteristic['value']
|
||||
elif ctype == "horizontal-tilt.current":
|
||||
self._chars['horizontal-tilt.current'] = characteristic['iid']
|
||||
if characteristic['value'] is not None:
|
||||
self._tilt_position = characteristic['value']
|
||||
elif ctype == "vertical-tilt.target":
|
||||
self._chars['vertical-tilt.target'] = \
|
||||
characteristic['iid']
|
||||
elif ctype == "horizontal-tilt.target":
|
||||
self._chars['vertical-tilt.target'] = \
|
||||
characteristic['iid']
|
||||
elif ctype == "obstruction-detected":
|
||||
self._chars['obstruction-detected'] = characteristic['iid']
|
||||
self._obstruction_detected = characteristic['value']
|
||||
elif ctype == "name":
|
||||
self._chars['name'] = characteristic['iid']
|
||||
if 'value' in characteristic:
|
||||
self._name = characteristic['value']
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the cover."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = (
|
||||
SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION)
|
||||
|
||||
if self._tilt_position is not None:
|
||||
supported_features |= (
|
||||
SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT |
|
||||
SUPPORT_SET_TILT_POSITION)
|
||||
|
||||
return supported_features
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return the current position of cover."""
|
||||
return self._position
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return true if cover is closed, else False."""
|
||||
return self._position == 0
|
||||
|
||||
@property
|
||||
def is_closing(self):
|
||||
"""Return if the cover is closing or not."""
|
||||
return self._state == STATE_CLOSING
|
||||
|
||||
@property
|
||||
def is_opening(self):
|
||||
"""Return if the cover is opening or not."""
|
||||
return self._state == STATE_OPENING
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Send open command."""
|
||||
self.set_cover_position(position=100)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Send close command."""
|
||||
self.set_cover_position(position=0)
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Send position command."""
|
||||
position = kwargs[ATTR_POSITION]
|
||||
characteristics = [{'aid': self._aid,
|
||||
'iid': self._chars['position.target'],
|
||||
'value': position}]
|
||||
self.put_characteristics(characteristics)
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self):
|
||||
"""Return current position of cover tilt."""
|
||||
return self._tilt_position
|
||||
|
||||
def set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
tilt_position = kwargs[ATTR_TILT_POSITION]
|
||||
if 'vertical-tilt.target' in self._chars:
|
||||
characteristics = [{'aid': self._aid,
|
||||
'iid': self._chars['vertical-tilt.target'],
|
||||
'value': tilt_position}]
|
||||
self.put_characteristics(characteristics)
|
||||
elif 'horizontal-tilt.target' in self._chars:
|
||||
characteristics = [{'aid': self._aid,
|
||||
'iid':
|
||||
self._chars['horizontal-tilt.target'],
|
||||
'value': tilt_position}]
|
||||
self.put_characteristics(characteristics)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
state_attributes = {}
|
||||
if self._obstruction_detected is not None:
|
||||
state_attributes['obstruction-detected'] = \
|
||||
self._obstruction_detected
|
||||
|
||||
if self._hold is not None:
|
||||
state_attributes['hold-position'] = \
|
||||
self._hold
|
||||
|
||||
return state_attributes
|
||||
@@ -46,7 +46,7 @@ COVER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
|
||||
vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA),
|
||||
})
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user