mirror of
https://github.com/home-assistant/core.git
synced 2026-01-02 20:14:30 +01:00
Compare commits
779 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4ae73ce38 | ||
|
|
58cde6b497 | ||
|
|
741d0fd09b | ||
|
|
bc9548fdaf | ||
|
|
35e9505ad5 | ||
|
|
38aa7d2c95 | ||
|
|
55a7ea6cc5 | ||
|
|
04bca7be6b | ||
|
|
8180797d2f | ||
|
|
0fe573ecc0 | ||
|
|
185595c113 | ||
|
|
ffdf48b15a | ||
|
|
fa4264be3f | ||
|
|
c7a34d0222 | ||
|
|
e054d68565 | ||
|
|
2dbe6d3289 | ||
|
|
ee107755f8 | ||
|
|
081a0290ba | ||
|
|
95ed8fb245 | ||
|
|
0d4858e296 | ||
|
|
f6a6be9a22 | ||
|
|
065b077369 | ||
|
|
1ec08ce243 | ||
|
|
46c955a501 | ||
|
|
c1429f5d80 | ||
|
|
ed16681b8e | ||
|
|
1ab03d9e15 | ||
|
|
ffcaeb4ef1 | ||
|
|
dd1e352d1d | ||
|
|
3bfb5b119a | ||
|
|
a269603e3b | ||
|
|
8ae2ce2299 | ||
|
|
700b8b2d0c | ||
|
|
806aba4a1a | ||
|
|
1128cf576f | ||
|
|
febdb72fb2 | ||
|
|
3c0146d382 | ||
|
|
9e76293141 | ||
|
|
6149c2877d | ||
|
|
2efe607b78 | ||
|
|
9fc271d178 | ||
|
|
06f76e8e97 | ||
|
|
d5bd8b9405 | ||
|
|
34c03109e5 | ||
|
|
df3ceb8d87 | ||
|
|
f514d44224 | ||
|
|
7fb0055a92 | ||
|
|
f81eeded90 | ||
|
|
6df31da180 | ||
|
|
364f5c8c02 | ||
|
|
85ac85c959 | ||
|
|
a2565ad3b4 | ||
|
|
6d31d56c03 | ||
|
|
ef28e2cc2a | ||
|
|
ff047e1cd1 | ||
|
|
8741a20191 | ||
|
|
d0c6f0b710 | ||
|
|
15c1213928 | ||
|
|
f618e7253a | ||
|
|
5df57bbda5 | ||
|
|
4433ad0a06 | ||
|
|
f18a49ce97 | ||
|
|
23cb053e82 | ||
|
|
52eb9e50aa | ||
|
|
35da3f053c | ||
|
|
1a4a9532dd | ||
|
|
2bb772bbdc | ||
|
|
ad5d4bbf51 | ||
|
|
9c9b25d4b9 | ||
|
|
89037b367b | ||
|
|
a3c3c41faa | ||
|
|
7eebf4631d | ||
|
|
decaabeb4a | ||
|
|
df02879c51 | ||
|
|
dbb49afb3e | ||
|
|
25a5bd32e2 | ||
|
|
3665e87800 | ||
|
|
fa0d538358 | ||
|
|
c508d5905b | ||
|
|
dc2cb62265 | ||
|
|
d0c3a8ecaf | ||
|
|
9986df358a | ||
|
|
e75820fc97 | ||
|
|
8e4e6a50d8 | ||
|
|
2e57d48191 | ||
|
|
a97fb8fd10 | ||
|
|
f7afd9d6bc | ||
|
|
88455a8a8b | ||
|
|
7d4083cdd3 | ||
|
|
7a9c9031af | ||
|
|
6a40a712cd | ||
|
|
3d85999258 | ||
|
|
5b33d952aa | ||
|
|
dcb4eb39fa | ||
|
|
073f947ca4 | ||
|
|
0b7e62f737 | ||
|
|
e114ae9b53 | ||
|
|
b909e5823f | ||
|
|
37ca9cabd1 | ||
|
|
1bfccd803f | ||
|
|
e02a5f0b31 | ||
|
|
4ed1d9ba8e | ||
|
|
d7183d642e | ||
|
|
308d1fbba0 | ||
|
|
ca524233ec | ||
|
|
3186109172 | ||
|
|
10e8f4f70a | ||
|
|
7b1cbeaf80 | ||
|
|
45e5f5de78 | ||
|
|
177ae3fd32 | ||
|
|
6bb95f6b58 | ||
|
|
c90219ad2e | ||
|
|
a45df7aac9 | ||
|
|
0e7a2f163c | ||
|
|
e7102eaf30 | ||
|
|
8b86bf7dd2 | ||
|
|
f48eb913b3 | ||
|
|
48138189b3 | ||
|
|
6a2da9f9a5 | ||
|
|
4d080f8b17 | ||
|
|
9e15fc1376 | ||
|
|
7bed448100 | ||
|
|
7a78d65633 | ||
|
|
60c787c2e6 | ||
|
|
dbcdc32f05 | ||
|
|
7251e29e60 | ||
|
|
8a4dd093f8 | ||
|
|
497038b332 | ||
|
|
f5878e1f22 | ||
|
|
6ab158ba88 | ||
|
|
d894025365 | ||
|
|
c341e33749 | ||
|
|
ec171b9928 | ||
|
|
e97b2b7015 | ||
|
|
2f89f88d23 | ||
|
|
b0d893afc9 | ||
|
|
23cb579f9f | ||
|
|
96f689a70f | ||
|
|
b804919eaa | ||
|
|
d722f4d64a | ||
|
|
930f75220c | ||
|
|
6b0180f753 | ||
|
|
1d2e9b6915 | ||
|
|
df580b2322 | ||
|
|
d1398e24be | ||
|
|
3368e30279 | ||
|
|
95662f82d4 | ||
|
|
c314220167 | ||
|
|
c6bc47b32d | ||
|
|
b390de1598 | ||
|
|
d99637e51b | ||
|
|
56b08a6ddb | ||
|
|
0a0975b5d9 | ||
|
|
39264af310 | ||
|
|
8c89e260df | ||
|
|
46ee7d7b22 | ||
|
|
e8343452cd | ||
|
|
fc481133e7 | ||
|
|
7a6950fd72 | ||
|
|
2527731865 | ||
|
|
479511ee42 | ||
|
|
2f17529f28 | ||
|
|
18cf8275b8 | ||
|
|
73a473ac29 | ||
|
|
3f69d0283d | ||
|
|
b767232e50 | ||
|
|
05f267de6e | ||
|
|
51508d69ad | ||
|
|
0a3e11aa12 | ||
|
|
f269135ae9 | ||
|
|
2c07bfb9e0 | ||
|
|
f7d4c48199 | ||
|
|
c8375be4b1 | ||
|
|
7d46ed0bf9 | ||
|
|
3d441dffad | ||
|
|
6c51592e34 | ||
|
|
c94b031db1 | ||
|
|
57f17707c6 | ||
|
|
7303d56a55 | ||
|
|
7e39e14086 | ||
|
|
8bfe77a1a0 | ||
|
|
ac7f1a7a37 | ||
|
|
02347df140 | ||
|
|
d078e50fb8 | ||
|
|
6ba9ccf052 | ||
|
|
8a81286abb | ||
|
|
f5c677146a | ||
|
|
f33bf718c7 | ||
|
|
c2cfc4a813 | ||
|
|
373b2009c9 | ||
|
|
e9d9861bda | ||
|
|
c81b1956da | ||
|
|
153c6957b9 | ||
|
|
7862fdd27e | ||
|
|
38d92b2abf | ||
|
|
6463b8165f | ||
|
|
72af4276b9 | ||
|
|
7624d0e79f | ||
|
|
fc7a187dd6 | ||
|
|
536356ceec | ||
|
|
984af45bb2 | ||
|
|
eab575e65d | ||
|
|
e7a17b710d | ||
|
|
a267df2abb | ||
|
|
f531ca61c6 | ||
|
|
9e56283eaf | ||
|
|
be51a3ae12 | ||
|
|
f4309dfcc6 | ||
|
|
691271147e | ||
|
|
3d5ee0eb58 | ||
|
|
6156bb4e5b | ||
|
|
7058249c01 | ||
|
|
fa8a4de019 | ||
|
|
bc5f0ff0b3 | ||
|
|
bbedf091aa | ||
|
|
5d3aac8130 | ||
|
|
a833736a1e | ||
|
|
6d2412022b | ||
|
|
51e6d5380e | ||
|
|
37f3eccb1e | ||
|
|
e48ef7f441 | ||
|
|
8582e390f8 | ||
|
|
c4e31bc4df | ||
|
|
6244a397b1 | ||
|
|
c82d2cb11c | ||
|
|
58ec77b017 | ||
|
|
4803f319b6 | ||
|
|
cac00f5b26 | ||
|
|
4110bd0acf | ||
|
|
0d2646ba25 | ||
|
|
34bb31f4ec | ||
|
|
a48c0f2991 | ||
|
|
f81ce0b720 | ||
|
|
88694c978b | ||
|
|
d48fe4cebc | ||
|
|
fd8d9747ef | ||
|
|
64ea13104e | ||
|
|
75bed93d3d | ||
|
|
6ee23bdf4e | ||
|
|
43487aa0d6 | ||
|
|
590eead128 | ||
|
|
1a05f7b04d | ||
|
|
38f063a158 | ||
|
|
d577955d1e | ||
|
|
55c8417ec0 | ||
|
|
49a2f5a40b | ||
|
|
5727beed8e | ||
|
|
6c53528ae8 | ||
|
|
c9ec166f4b | ||
|
|
36c135c785 | ||
|
|
d8c7160377 | ||
|
|
8cc5cc7f43 | ||
|
|
45a43592bd | ||
|
|
a0d6e08421 | ||
|
|
a4ffc9e37a | ||
|
|
8f9c2000ce | ||
|
|
a04d44d97a | ||
|
|
137d80452d | ||
|
|
3f73973970 | ||
|
|
3a79e37cde | ||
|
|
3f15b6b2d3 | ||
|
|
c5d4b7c243 | ||
|
|
236e484dc2 | ||
|
|
f51e8c3012 | ||
|
|
474fc21c66 | ||
|
|
82f6bed3a3 | ||
|
|
abb531c06b | ||
|
|
7a8aa79f19 | ||
|
|
ed9d1e776f | ||
|
|
d8119b2281 | ||
|
|
2d287d2abe | ||
|
|
4982c0b196 | ||
|
|
8bebd8583f | ||
|
|
3086e1d39d | ||
|
|
a40a0c4042 | ||
|
|
e407226afc | ||
|
|
abe85c73ae | ||
|
|
3fde1d3bab | ||
|
|
c7a49e0820 | ||
|
|
02b7fd93ed | ||
|
|
fa2e07d7c5 | ||
|
|
58220a9448 | ||
|
|
dbb42e5890 | ||
|
|
2a62906965 | ||
|
|
b1213b7a2d | ||
|
|
a4e7708450 | ||
|
|
439197ea3e | ||
|
|
f62d1d8d09 | ||
|
|
a91e79ee77 | ||
|
|
bb5c18f7be | ||
|
|
842534d472 | ||
|
|
83fb3637d9 | ||
|
|
6492809a7e | ||
|
|
3ce6be6297 | ||
|
|
c8eebb6b4a | ||
|
|
8c17b2f7dd | ||
|
|
353fca3b6e | ||
|
|
55619da722 | ||
|
|
87cabc933c | ||
|
|
a747eaa3ba | ||
|
|
6351c5c6ab | ||
|
|
8b3cf2d493 | ||
|
|
2b490e4486 | ||
|
|
a69b1a359d | ||
|
|
f004f440d3 | ||
|
|
dbe53a3947 | ||
|
|
a44966f483 | ||
|
|
192ed90773 | ||
|
|
8dfbfae270 | ||
|
|
144632a81b | ||
|
|
d1bf470899 | ||
|
|
7a33dc5cec | ||
|
|
008b641c56 | ||
|
|
b3e60df82a | ||
|
|
879967bed2 | ||
|
|
323dc5b78a | ||
|
|
c209236f47 | ||
|
|
9eb32728f1 | ||
|
|
5e7fdb479b | ||
|
|
9198047ad5 | ||
|
|
4b877dd96f | ||
|
|
cd3f51f7b1 | ||
|
|
e9d55bf1c0 | ||
|
|
5252c92670 | ||
|
|
dc185b994d | ||
|
|
74a7d4117e | ||
|
|
bab966fb29 | ||
|
|
79facb82c6 | ||
|
|
ec07affe0d | ||
|
|
193b608ee0 | ||
|
|
8eb93a8bea | ||
|
|
876b5fbe96 | ||
|
|
71e120ce97 | ||
|
|
82a1c0d0e8 | ||
|
|
8c657d4254 | ||
|
|
a75b151dfa | ||
|
|
563e4fbfca | ||
|
|
8b77298908 | ||
|
|
d15eedc0fb | ||
|
|
6c5f0b7434 | ||
|
|
be579b783a | ||
|
|
b130c433c9 | ||
|
|
eadc1e037a | ||
|
|
6996fec809 | ||
|
|
b50afec5f1 | ||
|
|
b9ec623ad9 | ||
|
|
96adbfdc36 | ||
|
|
0438dffe25 | ||
|
|
07d739c14e | ||
|
|
9bb88a6143 | ||
|
|
e29eb4fa23 | ||
|
|
172ede217a | ||
|
|
5d7c29dee2 | ||
|
|
754c4d205b | ||
|
|
beb6ddfa68 | ||
|
|
d231d59896 | ||
|
|
704983a64f | ||
|
|
f9564400e8 | ||
|
|
d5307c03d8 | ||
|
|
0c284161eb | ||
|
|
afac09932f | ||
|
|
8e39939b7e | ||
|
|
a5a926bcc6 | ||
|
|
d81a627739 | ||
|
|
cfe4cf30ad | ||
|
|
6aac49de7e | ||
|
|
a85bcce857 | ||
|
|
360caa3b1f | ||
|
|
98644135fa | ||
|
|
63d8dd9f7a | ||
|
|
685de23a4e | ||
|
|
b30c140648 | ||
|
|
2c10563205 | ||
|
|
5dd444fcd8 | ||
|
|
273007fa19 | ||
|
|
2e8c690033 | ||
|
|
836aab283f | ||
|
|
7cf92c2210 | ||
|
|
9eb4f89da4 | ||
|
|
167d8cbaba | ||
|
|
e90d980e67 | ||
|
|
81a659be0d | ||
|
|
51c7cbc6b9 | ||
|
|
14da2fd8c9 | ||
|
|
3872ac9bf9 | ||
|
|
b4fc1d77ea | ||
|
|
048b100eea | ||
|
|
625c8e0cee | ||
|
|
b797b1513a | ||
|
|
b1cca25299 | ||
|
|
7066fb0d10 | ||
|
|
58a89640bb | ||
|
|
7c5846aed2 | ||
|
|
a7d49e40c0 | ||
|
|
6a411710df | ||
|
|
f2941522ca | ||
|
|
e736521e9f | ||
|
|
4f2435103b | ||
|
|
3453d67cfe | ||
|
|
5613e8bb60 | ||
|
|
22d93a74a4 | ||
|
|
5651db4b5c | ||
|
|
3357596215 | ||
|
|
31ac965b16 | ||
|
|
e3ca1e6203 | ||
|
|
6d741d68b7 | ||
|
|
a5c7f131ee | ||
|
|
c7576999ca | ||
|
|
56c75d7706 | ||
|
|
e0b4e88544 | ||
|
|
6f345c55c9 | ||
|
|
5cb69cf163 | ||
|
|
471afb4702 | ||
|
|
8a86a79040 | ||
|
|
8a0b210f87 | ||
|
|
6c14e7afa7 | ||
|
|
429e2cdde8 | ||
|
|
d6e2862115 | ||
|
|
e00ae35e07 | ||
|
|
b8b3f4e88f | ||
|
|
0427154963 | ||
|
|
3bd37d6a65 | ||
|
|
16e0953f26 | ||
|
|
48189dd152 | ||
|
|
2578c8525b | ||
|
|
7646dc00e0 | ||
|
|
39eaa7fc8d | ||
|
|
1e26151069 | ||
|
|
e708032669 | ||
|
|
a7e613616c | ||
|
|
82296aeb71 | ||
|
|
e78709c5f5 | ||
|
|
1ce622469d | ||
|
|
ab2ac60d12 | ||
|
|
2e02efed10 | ||
|
|
bbc4775eab | ||
|
|
431cc63aaf | ||
|
|
0056fcf904 | ||
|
|
1e96d69688 | ||
|
|
9f2c5b7231 | ||
|
|
a5b03541e9 | ||
|
|
de4d1f2c19 | ||
|
|
6829ecad9d | ||
|
|
42e3e878df | ||
|
|
c96804954c | ||
|
|
282fd225c9 | ||
|
|
a61181b10c | ||
|
|
7bd8c0d39a | ||
|
|
fbb28c401e | ||
|
|
734a67ede0 | ||
|
|
0e42cb64d6 | ||
|
|
2e61ead4fd | ||
|
|
de16059365 | ||
|
|
a71fcfb6e5 | ||
|
|
8af70d5d19 | ||
|
|
ec9a58442b | ||
|
|
804f1d1cc8 | ||
|
|
9a4b0cfb9b | ||
|
|
3d8efd4200 | ||
|
|
50a0504e07 | ||
|
|
e085383d2d | ||
|
|
755571abe3 | ||
|
|
7d7b931163 | ||
|
|
5abfc84382 | ||
|
|
842a36dc9e | ||
|
|
1b0b5b4b8c | ||
|
|
800b1c7fe6 | ||
|
|
388d614e30 | ||
|
|
4d1633807c | ||
|
|
f6e9dd4832 | ||
|
|
54777a81bc | ||
|
|
71ecaa4385 | ||
|
|
e70931da67 | ||
|
|
9bf0f60784 | ||
|
|
2eafa5f81a | ||
|
|
34324afbde | ||
|
|
2e375aa802 | ||
|
|
64306922b1 | ||
|
|
ecba87179f | ||
|
|
b6ac964df3 | ||
|
|
906f0113ad | ||
|
|
1a39fb4de7 | ||
|
|
4b9e3258dc | ||
|
|
1bfe86b30d | ||
|
|
fe8e51e2e9 | ||
|
|
b04fd08cea | ||
|
|
95a7077b41 | ||
|
|
8e975395be | ||
|
|
b1a6539290 | ||
|
|
3ad4419cb6 | ||
|
|
f9f100b575 | ||
|
|
cc886821bc | ||
|
|
c05bff7d17 | ||
|
|
65c47824a0 | ||
|
|
fbb9097f6c | ||
|
|
e81e5ea796 | ||
|
|
c4a4af7c29 | ||
|
|
24095c0d7b | ||
|
|
ae18705c45 | ||
|
|
ab642ca4eb | ||
|
|
b7bc520a0e | ||
|
|
53595e76d8 | ||
|
|
173ef7cac5 | ||
|
|
9f72764cff | ||
|
|
77f7a53d9f | ||
|
|
fae8265a37 | ||
|
|
a95fb809a5 | ||
|
|
21917f4dc4 | ||
|
|
9aa5b904c6 | ||
|
|
c0ce86fa8e | ||
|
|
613c356c5f | ||
|
|
f46a8378b0 | ||
|
|
6401920019 | ||
|
|
a07919ced2 | ||
|
|
daf6b01b98 | ||
|
|
c31ab7a175 | ||
|
|
4e78d895d9 | ||
|
|
ec076c7c10 | ||
|
|
e7d3b22b46 | ||
|
|
75eeeae920 | ||
|
|
6dc127780e | ||
|
|
1050baa9cc | ||
|
|
5f6037d563 | ||
|
|
f4625fd561 | ||
|
|
424543f34a | ||
|
|
78047c8c3c | ||
|
|
e14dbfb006 | ||
|
|
01052f516b | ||
|
|
709419e465 | ||
|
|
ee8cd861e0 | ||
|
|
821a90fa54 | ||
|
|
8874422e8a | ||
|
|
59476ab475 | ||
|
|
8d86722c0e | ||
|
|
615b1cbfc7 | ||
|
|
3a406f5677 | ||
|
|
4db224ceb5 | ||
|
|
5d8d905822 | ||
|
|
a2c9834852 | ||
|
|
4f820aef83 | ||
|
|
6ba2891604 | ||
|
|
26726af689 | ||
|
|
9d21afa444 | ||
|
|
05cdab03b1 | ||
|
|
ece9c62ee8 | ||
|
|
a69080ba73 | ||
|
|
7741ec4d5a | ||
|
|
92457ca5ca | ||
|
|
8777146053 | ||
|
|
c4eab21736 | ||
|
|
f11f5255ae | ||
|
|
a1369c2fee | ||
|
|
8bf5e57b7f | ||
|
|
14ceb8472f | ||
|
|
e022f4465c | ||
|
|
b8e38c1b25 | ||
|
|
c7904a4b37 | ||
|
|
8f3434c2ab | ||
|
|
d13c892b28 | ||
|
|
26d4736ebf | ||
|
|
e670491c86 | ||
|
|
e26a5abb2b | ||
|
|
78e162c1d3 | ||
|
|
9176e13a97 | ||
|
|
217782cd05 | ||
|
|
d8817bb127 | ||
|
|
90c4f6f6e5 | ||
|
|
f795d03503 | ||
|
|
24c7c2aa6e | ||
|
|
2b48ecd5c5 | ||
|
|
71b800457b | ||
|
|
c3f090af17 | ||
|
|
29ad3961e5 | ||
|
|
52437f6246 | ||
|
|
646c4a7137 | ||
|
|
4de2efd07f | ||
|
|
6540114ec5 | ||
|
|
fa9a6f072e | ||
|
|
a55afa8119 | ||
|
|
19d99ddf57 | ||
|
|
1766536812 | ||
|
|
02b12ec1b9 | ||
|
|
80250add9e | ||
|
|
7e3567319f | ||
|
|
afa99c9189 | ||
|
|
7519e8d417 | ||
|
|
133ae63ed0 | ||
|
|
3fddf5df08 | ||
|
|
a27e821e8b | ||
|
|
c71e5ed588 | ||
|
|
2cebf9ef71 | ||
|
|
3cca3c37f0 | ||
|
|
77e7b63f4a | ||
|
|
65432ba552 | ||
|
|
bad0a8b342 | ||
|
|
baa4945944 | ||
|
|
e85b089eff | ||
|
|
a62c116959 | ||
|
|
6fa8fdf555 | ||
|
|
79445a7ccc | ||
|
|
73b38572f0 | ||
|
|
b2ba9d07ca | ||
|
|
8aef8c6bb4 | ||
|
|
0c4380a78d | ||
|
|
42c27e5b72 | ||
|
|
5ad3e75a4d | ||
|
|
6ffe9ad473 | ||
|
|
6a74c403c0 | ||
|
|
2731777c7e | ||
|
|
a59487a438 | ||
|
|
f1a0ad9e4a | ||
|
|
b57d809dad | ||
|
|
f997957054 | ||
|
|
c8048e1aff | ||
|
|
17a96c6d9b | ||
|
|
96133f5e6b | ||
|
|
af4b85d39d | ||
|
|
324a7c7875 | ||
|
|
c59d45caa3 | ||
|
|
f272ed3b91 | ||
|
|
548371e94c | ||
|
|
1aee7a1673 | ||
|
|
b6987a1235 | ||
|
|
d1f75fcf32 | ||
|
|
adca598172 | ||
|
|
7f940423ad | ||
|
|
1b0e523a60 | ||
|
|
0d46e2c0b5 | ||
|
|
d2a83c2732 | ||
|
|
dc64634e21 | ||
|
|
0ae38aece8 | ||
|
|
88df2e0ea5 | ||
|
|
7421156dfc | ||
|
|
d5732c4dba | ||
|
|
eabb68ad7d | ||
|
|
8d1cf553de | ||
|
|
89f8203163 | ||
|
|
ed93c3b2c1 | ||
|
|
6988fe783c | ||
|
|
9214934d47 | ||
|
|
71ebc4f594 | ||
|
|
c5f4aa0466 | ||
|
|
49b92b5349 | ||
|
|
1ddc249989 | ||
|
|
4c4eff1d62 | ||
|
|
16dbf9b2ea | ||
|
|
c68b621972 | ||
|
|
d81df1f0ae | ||
|
|
112ed88d64 | ||
|
|
5f34d3ccb9 | ||
|
|
3c811bbf1a | ||
|
|
90dfe72d31 | ||
|
|
89221bfab9 | ||
|
|
773c567563 | ||
|
|
58c23bc2d9 | ||
|
|
611597a87b | ||
|
|
ecabf92504 | ||
|
|
e9cd9f88be | ||
|
|
a58f1eedda | ||
|
|
ce550206a4 | ||
|
|
1ddc65a0ce | ||
|
|
2b6e197deb | ||
|
|
b125514655 | ||
|
|
c90e13bfef | ||
|
|
30d4a9f12c | ||
|
|
3432c5da9e | ||
|
|
5716d0aa1a | ||
|
|
86a510441c | ||
|
|
bc85d47878 | ||
|
|
72bb94de96 | ||
|
|
0a4251e08f | ||
|
|
1b66520e31 | ||
|
|
81bb928394 | ||
|
|
fe468ace34 | ||
|
|
6526c68e2d | ||
|
|
f64a99878f | ||
|
|
03855c18fc | ||
|
|
2b02c0d0fc | ||
|
|
07dc23a0e3 | ||
|
|
77635d40e2 | ||
|
|
f4102339c1 | ||
|
|
21871b3d6b | ||
|
|
88be786e82 | ||
|
|
4b1de61110 | ||
|
|
ab17b22239 | ||
|
|
423d595edf | ||
|
|
e044eace20 | ||
|
|
9653544144 | ||
|
|
01d8b5831e | ||
|
|
62c2bbd59a | ||
|
|
398281959a | ||
|
|
db07e45df8 | ||
|
|
0344c761fc | ||
|
|
6cb8806085 | ||
|
|
2b250a7ec8 | ||
|
|
08849fd3e8 | ||
|
|
92dc26bab3 | ||
|
|
88669c6543 | ||
|
|
350904870e | ||
|
|
1499485a71 | ||
|
|
bf4b7a82b4 | ||
|
|
c2aa06d0d4 | ||
|
|
188293770e | ||
|
|
12df14b87b | ||
|
|
f195ecca4b | ||
|
|
46ece3603f | ||
|
|
05db444832 | ||
|
|
ecfe0fc3dd | ||
|
|
e5a2ef9b8d | ||
|
|
9591aa66ba | ||
|
|
af473cddf0 | ||
|
|
9c7ef13f91 | ||
|
|
9f96aab2f4 | ||
|
|
ce5cf5803c | ||
|
|
e14b243336 | ||
|
|
29131a655d | ||
|
|
c020b7c47d | ||
|
|
8529ad3ba1 | ||
|
|
0d42ed1861 | ||
|
|
ba923d2d66 | ||
|
|
9b1491a98d | ||
|
|
1aab551eed | ||
|
|
54dfc3e2b4 | ||
|
|
cf5ba7d922 | ||
|
|
d16c507f34 | ||
|
|
54489a3514 | ||
|
|
f5076188ef | ||
|
|
d33cad0b24 | ||
|
|
4423572682 | ||
|
|
c90f0d5bd6 | ||
|
|
0466e43478 | ||
|
|
7807b40925 | ||
|
|
9e4bd88a06 | ||
|
|
b4f8d157d6 | ||
|
|
ade86b9b8d | ||
|
|
179c2315be | ||
|
|
f396de623b | ||
|
|
d0365f5911 | ||
|
|
dbdf5558e6 | ||
|
|
42265036ff | ||
|
|
53b204347d | ||
|
|
dc656205c4 | ||
|
|
087748b5ff | ||
|
|
17ba33004c | ||
|
|
9520d38288 | ||
|
|
cf69f25354 | ||
|
|
101225749b | ||
|
|
e581d9e249 | ||
|
|
3ce50b0a6a | ||
|
|
941f9b29dc | ||
|
|
3b34594aa3 | ||
|
|
5a9e8b2d3e | ||
|
|
89c96279ce | ||
|
|
851378739f | ||
|
|
9575c20b7c | ||
|
|
dcaced1966 | ||
|
|
6a80ffa8cc | ||
|
|
3769f5893a | ||
|
|
62f12d242a | ||
|
|
b25e951dcc | ||
|
|
8d2d71c16a | ||
|
|
4e84e8a15e | ||
|
|
018a5d5c1f | ||
|
|
bd930b6e96 | ||
|
|
ef2e3f607a | ||
|
|
e480f75d6d | ||
|
|
300384410f | ||
|
|
b022428cb6 | ||
|
|
2b25c25ca8 | ||
|
|
4ff8a46acf | ||
|
|
abf2e763b1 | ||
|
|
6381242eca | ||
|
|
e75b12b92a | ||
|
|
95da6d41f9 | ||
|
|
3fcfba0a1e | ||
|
|
2787671de5 | ||
|
|
673c8907e3 | ||
|
|
5ef602bc2e | ||
|
|
acc44aaf6c |
272
.circleci/config.yml
Normal file
272
.circleci/config.yml
Normal file
@@ -0,0 +1,272 @@
|
||||
# Python CircleCI 2.0 configuration file
|
||||
#
|
||||
# Check https://circleci.com/docs/2.0/language-python/ for more details
|
||||
#
|
||||
version: 2.1
|
||||
|
||||
executors:
|
||||
|
||||
python:
|
||||
parameters:
|
||||
tag:
|
||||
type: string
|
||||
default: latest
|
||||
docker:
|
||||
- image: circleci/python:<< parameters.tag >>
|
||||
- image: circleci/buildpack-deps:stretch
|
||||
working_directory: ~/repo
|
||||
|
||||
commands:
|
||||
|
||||
docker-prereqs:
|
||||
description: Set up docker prerequisite requirement
|
||||
steps:
|
||||
- run: sudo apt-get update && sudo apt-get install -y --no-install-recommends
|
||||
libudev-dev libavformat-dev libavcodec-dev libavdevice-dev libavutil-dev
|
||||
libswscale-dev libswresample-dev libavfilter-dev
|
||||
|
||||
install-requirements:
|
||||
description: Set up venv and install requirements python packages with cache support
|
||||
parameters:
|
||||
python:
|
||||
type: string
|
||||
default: latest
|
||||
all:
|
||||
description: pip install -r requirements_all.txt
|
||||
type: boolean
|
||||
default: false
|
||||
test:
|
||||
description: pip install -r requirements_test.txt
|
||||
type: boolean
|
||||
default: false
|
||||
test_all:
|
||||
description: pip install -r requirements_test_all.txt
|
||||
type: boolean
|
||||
default: false
|
||||
steps:
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-<< parameters.python >>-{{ checksum "homeassistant/package_constraints.txt" }}-<<# parameters.all >>{{ checksum "requirements_all.txt" }}<</ parameters.all>>-<<# parameters.test >>{{ checksum "requirements_test.txt" }}<</ parameters.test>>-<<# parameters.test_all >>{{ checksum "requirements_test_all.txt" }}<</ parameters.test_all>>
|
||||
- run:
|
||||
name: install dependencies
|
||||
command: |
|
||||
python3 -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install -q -U pip
|
||||
pip install -q -U setuptools
|
||||
<<# parameters.all >>pip install -q --progress-bar off -r requirements_all.txt -c homeassistant/package_constraints.txt<</ parameters.all>>
|
||||
<<# parameters.test >>pip install -q --progress-bar off -r requirements_test.txt -c homeassistant/package_constraints.txt<</ parameters.test>>
|
||||
<<# parameters.test_all >>pip install -q --progress-bar off -r requirements_test_all.txt -c homeassistant/package_constraints.txt<</ parameters.test_all>>
|
||||
no_output_timeout: 15m
|
||||
- save_cache:
|
||||
paths:
|
||||
- ./venv
|
||||
key: v1-<< parameters.python >>-{{ checksum "homeassistant/package_constraints.txt" }}-<<# parameters.all >>{{ checksum "requirements_all.txt" }}<</ parameters.all>>-<<# parameters.test >>{{ checksum "requirements_test.txt" }}<</ parameters.test>>-<<# parameters.test_all >>{{ checksum "requirements_test_all.txt" }}<</ parameters.test_all>>
|
||||
|
||||
install:
|
||||
description: Install Home Assistant
|
||||
steps:
|
||||
- run:
|
||||
name: install
|
||||
command: |
|
||||
. venv/bin/activate
|
||||
pip install -q --progress-bar off -e .
|
||||
|
||||
jobs:
|
||||
|
||||
static-check:
|
||||
executor:
|
||||
name: python
|
||||
tag: 3.5.5-stretch
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- docker-prereqs
|
||||
- install-requirements:
|
||||
python: 3.5.5-stretch
|
||||
test: true
|
||||
|
||||
- run:
|
||||
name: run static check
|
||||
command: |
|
||||
. venv/bin/activate
|
||||
flake8
|
||||
|
||||
- run:
|
||||
name: run static type check
|
||||
command: |
|
||||
. venv/bin/activate
|
||||
TYPING_FILES=$(cat mypyrc)
|
||||
mypy $TYPING_FILES
|
||||
|
||||
- install
|
||||
|
||||
- run:
|
||||
name: validate manifests
|
||||
command: |
|
||||
. venv/bin/activate
|
||||
python -m script.hassfest validate
|
||||
|
||||
- run:
|
||||
name: run gen_requirements_all
|
||||
command: |
|
||||
. venv/bin/activate
|
||||
python script/gen_requirements_all.py validate
|
||||
|
||||
pre-install-all-requirements:
|
||||
executor:
|
||||
name: python
|
||||
tag: 3.5.5-stretch
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- docker-prereqs
|
||||
- install-requirements:
|
||||
python: 3.5.5-stretch
|
||||
all: true
|
||||
test: true
|
||||
|
||||
pylint:
|
||||
executor:
|
||||
name: python
|
||||
tag: 3.5.5-stretch
|
||||
parallelism: 2
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- docker-prereqs
|
||||
- install-requirements:
|
||||
python: 3.5.5-stretch
|
||||
all: true
|
||||
test: true
|
||||
- install
|
||||
|
||||
- run:
|
||||
name: run pylint
|
||||
command: |
|
||||
. venv/bin/activate
|
||||
PYFILES=$(circleci tests glob "homeassistant/**/*.py" | circleci tests split)
|
||||
pylint ${PYFILES}
|
||||
no_output_timeout: 15m
|
||||
|
||||
pre-test:
|
||||
parameters:
|
||||
python:
|
||||
type: string
|
||||
executor:
|
||||
name: python
|
||||
tag: << parameters.python >>
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- docker-prereqs
|
||||
- install-requirements:
|
||||
python: << parameters.python >>
|
||||
test_all: true
|
||||
|
||||
test:
|
||||
parameters:
|
||||
python:
|
||||
type: string
|
||||
executor:
|
||||
name: python
|
||||
tag: << parameters.python >>
|
||||
parallelism: 2
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- docker-prereqs
|
||||
- install-requirements:
|
||||
python: << parameters.python >>
|
||||
test_all: true
|
||||
- install
|
||||
|
||||
- run:
|
||||
name: run tests with code coverage
|
||||
command: |
|
||||
. venv/bin/activate
|
||||
CC_SWITCH="--cov --cov-report="
|
||||
TESTFILES=$(circleci tests glob "tests/**/test_*.py" | circleci tests split --split-by=timings)
|
||||
pytest --timeout=9 --durations=10 --junitxml=test-reports/homeassistant/results.xml -qq -o junit_family=xunit2 -o junit_suite_name=homeassistant -o console_output_style=count -p no:sugar $CC_SWITCH -- ${TESTFILES}
|
||||
script/check_dirty
|
||||
codecov
|
||||
|
||||
- store_test_results:
|
||||
path: test-reports
|
||||
|
||||
- store_artifacts:
|
||||
path: htmlcov
|
||||
destination: cov-reports
|
||||
|
||||
- store_artifacts:
|
||||
path: test-reports
|
||||
destination: test-reports
|
||||
|
||||
# This job use machine executor, e.g. classic CircleCI VM because we need both lokalise-cli and a Python runtime.
|
||||
# Classic CircleCI included python 2.7.12 and python 3.5.2 managed by pyenv, the Python version may need change if
|
||||
# CircleCI changed its VM in future.
|
||||
upload-translations:
|
||||
machine: true
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
name: upload english translations
|
||||
command: |
|
||||
pyenv versions
|
||||
pyenv global 3.5.2
|
||||
docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21
|
||||
script/translations_upload
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- static-check
|
||||
- pre-install-all-requirements:
|
||||
requires:
|
||||
- static-check
|
||||
- pylint:
|
||||
requires:
|
||||
- pre-install-all-requirements
|
||||
- pre-test:
|
||||
name: pre-test 3.5.5
|
||||
requires:
|
||||
- static-check
|
||||
python: 3.5.5-stretch
|
||||
- pre-test:
|
||||
name: pre-test 3.6
|
||||
requires:
|
||||
- static-check
|
||||
python: 3.6-stretch
|
||||
- pre-test:
|
||||
name: pre-test 3.7
|
||||
requires:
|
||||
- static-check
|
||||
python: 3.7-stretch
|
||||
- test:
|
||||
name: test 3.5.5
|
||||
requires:
|
||||
- pre-test 3.5.5
|
||||
python: 3.5.5-stretch
|
||||
- test:
|
||||
name: test 3.6
|
||||
requires:
|
||||
- pre-test 3.6
|
||||
python: 3.6-stretch
|
||||
- test:
|
||||
name: test 3.7
|
||||
requires:
|
||||
- pre-test 3.7
|
||||
python: 3.7-stretch
|
||||
# CircleCI does not allow failure yet
|
||||
# - test:
|
||||
# name: test 3.8
|
||||
# python: 3.8-rc-stretch
|
||||
- upload-translations:
|
||||
requires:
|
||||
- static-check
|
||||
filters:
|
||||
branches:
|
||||
only: dev
|
||||
15
.codecov.yml
Normal file
15
.codecov.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
codecov:
|
||||
branch: dev
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 90
|
||||
threshold: 0.09
|
||||
notify:
|
||||
# Notify codecov room in Discord. The webhook URL (encrypted below) ends in /slack which is why we configure a Slack notification.
|
||||
slack:
|
||||
default:
|
||||
url: "secret:TgWDUM4Jw0w7wMJxuxNF/yhSOHglIo1fGwInJnRLEVPy2P2aLimkoK1mtKCowH5TFw+baUXVXT3eAqefbdvIuM8BjRR4aRji95C6CYyD0QHy4N8i7nn1SQkWDPpS8IthYTg07rUDF7s5guurkKv2RrgoCdnnqjAMSzHoExMOF7xUmblMdhBTWJgBpWEhASJy85w/xxjlsE1xoTkzeJu9Q67pTXtRcn+5kb5/vIzPSYg="
|
||||
comment:
|
||||
require_changes: yes
|
||||
939
.coveragerc
939
.coveragerc
File diff suppressed because it is too large
Load Diff
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -23,7 +23,8 @@ If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
|
||||
|
||||
If the code communicates with devices, web services, or third-party tools:
|
||||
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
|
||||
- [ ] [_The manifest file_][manifest-docs] has all fields filled out correctly ([example][ex-manifest]).
|
||||
- [ ] New dependencies have been added to `requirements` in the manifest ([example][ex-requir]).
|
||||
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
|
||||
- [ ] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.
|
||||
- [ ] New files were added to `.coveragerc`.
|
||||
@@ -31,5 +32,7 @@ If the code communicates with devices, web services, or third-party tools:
|
||||
If the code does not interact with devices:
|
||||
- [ ] Tests have been added to verify that the new code works.
|
||||
|
||||
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard/__init__.py#L14
|
||||
[ex-manifest]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/mobile_app/manifest.json
|
||||
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/mobile_app/manifest.json#L5
|
||||
[ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard/__init__.py#L23
|
||||
[manifest-docs]: https://developers.home-assistant.io/docs/en/development_checklist.html#_the-manifest-file_
|
||||
|
||||
45
.github/main.workflow
vendored
45
.github/main.workflow
vendored
@@ -1,41 +1,14 @@
|
||||
workflow "Python 3.7 - tox" {
|
||||
resolves = ["Python 3.7 - tests"]
|
||||
on = "push"
|
||||
workflow "Mention CODEOWNERS of integrations when integration label is added to an issue" {
|
||||
on = "issues"
|
||||
resolves = "codeowners-mention"
|
||||
}
|
||||
|
||||
action "Python 3.7 - tests" {
|
||||
uses = "home-assistant/actions/py37-tox@master"
|
||||
args = "-e py37"
|
||||
workflow "Mention CODEOWNERS of integrations when integration label is added to an PRs" {
|
||||
on = "pull_request"
|
||||
resolves = "codeowners-mention"
|
||||
}
|
||||
|
||||
workflow "Python 3.6 - tox" {
|
||||
resolves = ["Python 3.6 - tests"]
|
||||
on = "push"
|
||||
}
|
||||
|
||||
action "Python 3.6 - tests" {
|
||||
uses = "home-assistant/actions/py36-tox@master"
|
||||
args = "-e py36"
|
||||
}
|
||||
|
||||
workflow "Python 3.5 - tox" {
|
||||
resolves = ["Pyton 3.5 - typing"]
|
||||
on = "push"
|
||||
}
|
||||
|
||||
action "Python 3.5 - tests" {
|
||||
uses = "home-assistant/actions/py35-tox@master"
|
||||
args = "-e py35"
|
||||
}
|
||||
|
||||
action "Python 3.5 - lints" {
|
||||
uses = "home-assistant/actions/py35-tox@master"
|
||||
needs = ["Python 3.5 - tests"]
|
||||
args = "-e lint"
|
||||
}
|
||||
|
||||
action "Pyton 3.5 - typing" {
|
||||
uses = "home-assistant/actions/py35-tox@master"
|
||||
args = "-e typing"
|
||||
needs = ["Python 3.5 - lints"]
|
||||
action "codeowners-mention" {
|
||||
uses = "home-assistant/codeowners-mention@master"
|
||||
secrets = ["GITHUB_TOKEN"]
|
||||
}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -55,6 +55,7 @@ pip-log.txt
|
||||
.tox
|
||||
nosetests.xml
|
||||
htmlcov/
|
||||
test-reports/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
55
.travis.yml
55
.travis.yml
@@ -1,55 +0,0 @@
|
||||
sudo: false
|
||||
dist: xenial
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: "ppa:jonathonf/ffmpeg-4"
|
||||
packages:
|
||||
- libudev-dev
|
||||
- libavformat-dev
|
||||
- libavcodec-dev
|
||||
- libavdevice-dev
|
||||
- libavutil-dev
|
||||
- libswscale-dev
|
||||
- libswresample-dev
|
||||
- libavfilter-dev
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=lint
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=pylint
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=typing
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=cov
|
||||
after_success: coveralls
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
- python: "3.7"
|
||||
env: TOXENV=py37
|
||||
- python: "3.8-dev"
|
||||
env: TOXENV=py38
|
||||
if: branch = dev AND type = push
|
||||
allow_failures:
|
||||
- python: "3.8-dev"
|
||||
env: TOXENV=py38
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
install: pip install -U tox coveralls
|
||||
language: python
|
||||
script: travis_wait 40 tox --develop
|
||||
services:
|
||||
- docker
|
||||
before_deploy:
|
||||
- docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21
|
||||
deploy:
|
||||
skip_cleanup: true
|
||||
provider: script
|
||||
script: script/travis_deploy
|
||||
on:
|
||||
branch: dev
|
||||
condition: $TOXENV = lint
|
||||
387
CODEOWNERS
387
CODEOWNERS
@@ -1,3 +1,4 @@
|
||||
# This file is generated by script/manifest/codeowners.py
|
||||
# People marked here will be automatically requested for a review
|
||||
# when the code that they own is touched.
|
||||
# https://github.com/blog/2392-introducing-code-owners
|
||||
@@ -7,259 +8,255 @@ setup.py @home-assistant/core
|
||||
homeassistant/*.py @home-assistant/core
|
||||
homeassistant/helpers/* @home-assistant/core
|
||||
homeassistant/util/* @home-assistant/core
|
||||
homeassistant/components/api/* @home-assistant/core
|
||||
homeassistant/components/auth/* @home-assistant/core
|
||||
homeassistant/components/automation/* @home-assistant/core
|
||||
homeassistant/components/cloud/* @home-assistant/core
|
||||
homeassistant/components/config/* @home-assistant/core
|
||||
homeassistant/components/configurator/* @home-assistant/core
|
||||
homeassistant/components/conversation/* @home-assistant/core
|
||||
homeassistant/components/frontend/* @home-assistant/core
|
||||
homeassistant/components/group/* @home-assistant/core
|
||||
homeassistant/components/history/* @home-assistant/core
|
||||
homeassistant/components/http/* @home-assistant/core
|
||||
homeassistant/components/input_*.py @home-assistant/core
|
||||
homeassistant/components/introduction/* @home-assistant/core
|
||||
homeassistant/components/logger/* @home-assistant/core
|
||||
homeassistant/components/lovelace/* @home-assistant/core
|
||||
homeassistant/components/mqtt/* @home-assistant/core
|
||||
homeassistant/components/panel_custom/* @home-assistant/core
|
||||
homeassistant/components/panel_iframe/* @home-assistant/core
|
||||
homeassistant/components/onboarding/* @home-assistant/core
|
||||
homeassistant/components/persistent_notification/* @home-assistant/core
|
||||
homeassistant/components/scene/__init__.py @home-assistant/core
|
||||
homeassistant/components/scene/homeassistant.py @home-assistant/core
|
||||
homeassistant/components/script/* @home-assistant/core
|
||||
homeassistant/components/shell_command/* @home-assistant/core
|
||||
homeassistant/components/sun/* @home-assistant/core
|
||||
homeassistant/components/updater/* @home-assistant/core
|
||||
homeassistant/components/weblink/* @home-assistant/core
|
||||
homeassistant/components/websocket_api/* @home-assistant/core
|
||||
homeassistant/components/zone/* @home-assistant/core
|
||||
|
||||
# Home Assistant Developer Teams
|
||||
# Virtualization
|
||||
Dockerfile @home-assistant/docker
|
||||
virtualization/Docker/* @home-assistant/docker
|
||||
|
||||
homeassistant/components/zwave/* @home-assistant/z-wave
|
||||
homeassistant/components/*/zwave.py @home-assistant/z-wave
|
||||
# Other code
|
||||
homeassistant/scripts/check_config.py @kellerza
|
||||
|
||||
homeassistant/components/hassio/* @home-assistant/hassio
|
||||
|
||||
# Individual platforms
|
||||
homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
|
||||
homeassistant/components/binary_sensor/hikvision.py @mezz64
|
||||
homeassistant/components/binary_sensor/threshold.py @fabaff
|
||||
homeassistant/components/binary_sensor/uptimerobot.py @ludeeus
|
||||
homeassistant/components/camera/push.py @dgomes
|
||||
homeassistant/components/camera/yi.py @bachya
|
||||
homeassistant/components/climate/coolmaster.py @OnFreund
|
||||
homeassistant/components/climate/ephember.py @ttroy50
|
||||
homeassistant/components/climate/eq3btsmart.py @rytilahti
|
||||
homeassistant/components/climate/mill.py @danielhiversen
|
||||
homeassistant/components/climate/sensibo.py @andrey-git
|
||||
homeassistant/components/cover/brunt.py @eavanvalkenburg
|
||||
homeassistant/components/cover/group.py @cdce8p
|
||||
homeassistant/components/cover/template.py @PhracturedBlue
|
||||
homeassistant/components/device_tracker/asuswrt.py @kennedyshead
|
||||
homeassistant/components/device_tracker/automatic.py @armills
|
||||
homeassistant/components/device_tracker/bt_smarthub.py @jxwolstenholme
|
||||
homeassistant/components/device_tracker/huawei_router.py @abmantis
|
||||
homeassistant/components/device_tracker/quantum_gateway.py @cisasteelersfan
|
||||
homeassistant/components/device_tracker/tile.py @bachya
|
||||
homeassistant/components/device_tracker/traccar.py @ludeeus
|
||||
homeassistant/components/device_tracker/synology_srm.py @aerialls
|
||||
homeassistant/components/device_tracker/xfinity.py @cisasteelersfan
|
||||
homeassistant/components/light/lifx_legacy.py @amelchio
|
||||
homeassistant/components/light/yeelight.py @rytilahti
|
||||
homeassistant/components/light/yeelightsunflower.py @lindsaymarkward
|
||||
homeassistant/components/lock/nello.py @pschmitt
|
||||
homeassistant/components/lock/nuki.py @pschmitt
|
||||
homeassistant/components/media_player/emby.py @mezz64
|
||||
homeassistant/components/media_player/kodi.py @armills
|
||||
homeassistant/components/media_player/liveboxplaytv.py @pschmitt
|
||||
homeassistant/components/media_player/mediaroom.py @dgomes
|
||||
homeassistant/components/media_player/monoprice.py @etsinko
|
||||
homeassistant/components/media_player/mpd.py @fabaff
|
||||
homeassistant/components/media_player/xiaomi_tv.py @fattdev
|
||||
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
|
||||
homeassistant/components/notify/file.py @fabaff
|
||||
homeassistant/components/notify/flock.py @fabaff
|
||||
homeassistant/components/notify/mastodon.py @fabaff
|
||||
homeassistant/components/notify/smtp.py @fabaff
|
||||
homeassistant/components/notify/syslog.py @fabaff
|
||||
homeassistant/components/notify/xmpp.py @fabaff
|
||||
homeassistant/components/notify/yessssms.py @flowolf
|
||||
homeassistant/components/scene/lifx_cloud.py @amelchio
|
||||
homeassistant/components/sensor/airvisual.py @bachya
|
||||
homeassistant/components/sensor/alpha_vantage.py @fabaff
|
||||
homeassistant/components/sensor/bitcoin.py @fabaff
|
||||
homeassistant/components/sensor/cpuspeed.py @fabaff
|
||||
homeassistant/components/sensor/cups.py @fabaff
|
||||
homeassistant/components/sensor/darksky.py @fabaff
|
||||
homeassistant/components/sensor/discogs.py @thibmaek
|
||||
homeassistant/components/sensor/file.py @fabaff
|
||||
homeassistant/components/sensor/filter.py @dgomes
|
||||
homeassistant/components/sensor/fixer.py @fabaff
|
||||
homeassistant/components/sensor/flunearyou.py @bachya
|
||||
homeassistant/components/sensor/gearbest.py @HerrHofrat
|
||||
homeassistant/components/sensor/gitter.py @fabaff
|
||||
homeassistant/components/sensor/glances.py @fabaff
|
||||
homeassistant/components/sensor/gpsd.py @fabaff
|
||||
homeassistant/components/sensor/integration.py @dgomes
|
||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||
homeassistant/components/sensor/jewish_calendar.py @tsvi
|
||||
homeassistant/components/sensor/launch_library.py @ludeeus
|
||||
homeassistant/components/sensor/linux_battery.py @fabaff
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/sensor/min_max.py @fabaff
|
||||
homeassistant/components/sensor/moon.py @fabaff
|
||||
homeassistant/components/sensor/netdata.py @fabaff
|
||||
homeassistant/components/sensor/nmbs.py @thibmaek
|
||||
homeassistant/components/sensor/nsw_fuel_station.py @nickw444
|
||||
homeassistant/components/sensor/pi_hole.py @fabaff
|
||||
homeassistant/components/sensor/pollen.py @bachya
|
||||
homeassistant/components/sensor/pvoutput.py @fabaff
|
||||
homeassistant/components/sensor/qnap.py @colinodell
|
||||
homeassistant/components/sensor/ruter.py @ludeeus
|
||||
homeassistant/components/sensor/scrape.py @fabaff
|
||||
homeassistant/components/sensor/serial.py @fabaff
|
||||
homeassistant/components/sensor/seventeentrack.py @bachya
|
||||
homeassistant/components/sensor/shodan.py @fabaff
|
||||
homeassistant/components/sensor/sma.py @kellerza
|
||||
homeassistant/components/sensor/sql.py @dgomes
|
||||
homeassistant/components/sensor/statistics.py @fabaff
|
||||
homeassistant/components/sensor/swiss*.py @fabaff
|
||||
homeassistant/components/sensor/sytadin.py @gautric
|
||||
homeassistant/components/sensor/tautulli.py @ludeeus
|
||||
homeassistant/components/sensor/time_date.py @fabaff
|
||||
homeassistant/components/sensor/version.py @fabaff
|
||||
homeassistant/components/sensor/waqi.py @andrey-git
|
||||
homeassistant/components/sensor/worldclock.py @fabaff
|
||||
homeassistant/components/switch/switchbot.py @danielhiversen
|
||||
homeassistant/components/switch/switchmate.py @danielhiversen
|
||||
homeassistant/components/vacuum/roomba.py @pschmitt
|
||||
homeassistant/components/weather/__init__.py @fabaff
|
||||
homeassistant/components/weather/darksky.py @fabaff
|
||||
homeassistant/components/weather/demo.py @fabaff
|
||||
homeassistant/components/weather/met.py @danielhiversen
|
||||
homeassistant/components/weather/openweathermap.py @fabaff
|
||||
|
||||
# A
|
||||
# Integrations
|
||||
homeassistant/components/airvisual/* @bachya
|
||||
homeassistant/components/alarm_control_panel/* @colinodell
|
||||
homeassistant/components/alpha_vantage/* @fabaff
|
||||
homeassistant/components/amazon_polly/* @robbiet480
|
||||
homeassistant/components/ambient_station/* @bachya
|
||||
homeassistant/components/api/* @home-assistant/core
|
||||
homeassistant/components/arduino/* @fabaff
|
||||
homeassistant/components/arest/* @fabaff
|
||||
homeassistant/components/asuswrt/* @kennedyshead
|
||||
homeassistant/components/auth/* @home-assistant/core
|
||||
homeassistant/components/automatic/* @armills
|
||||
homeassistant/components/automation/* @home-assistant/core
|
||||
homeassistant/components/aws/* @awarecan @robbiet480
|
||||
homeassistant/components/axis/* @kane610
|
||||
homeassistant/components/*/arest.py @fabaff
|
||||
|
||||
# B
|
||||
homeassistant/components/bitcoin/* @fabaff
|
||||
homeassistant/components/blink/* @fronzbot
|
||||
homeassistant/components/bmw_connected_drive/* @ChristianKuehnel
|
||||
homeassistant/components/*/broadlink.py @danielhiversen
|
||||
|
||||
# C
|
||||
homeassistant/components/braviatv/* @robbiet480
|
||||
homeassistant/components/broadlink/* @danielhiversen
|
||||
homeassistant/components/brunt/* @eavanvalkenburg
|
||||
homeassistant/components/bt_smarthub/* @jxwolstenholme
|
||||
homeassistant/components/cisco_ios/* @fbradyirl
|
||||
homeassistant/components/cisco_mobility_express/* @fbradyirl
|
||||
homeassistant/components/cisco_webex_teams/* @fbradyirl
|
||||
homeassistant/components/ciscospark/* @fbradyirl
|
||||
homeassistant/components/cloud/* @home-assistant/core
|
||||
homeassistant/components/cloudflare/* @ludeeus
|
||||
homeassistant/components/config/* @home-assistant/core
|
||||
homeassistant/components/configurator/* @home-assistant/core
|
||||
homeassistant/components/conversation/* @home-assistant/core
|
||||
homeassistant/components/coolmaster/* @OnFreund
|
||||
homeassistant/components/counter/* @fabaff
|
||||
|
||||
# D
|
||||
homeassistant/components/cover/* @home-assistant/core
|
||||
homeassistant/components/cpuspeed/* @fabaff
|
||||
homeassistant/components/cups/* @fabaff
|
||||
homeassistant/components/daikin/* @fredrike @rofrantz
|
||||
homeassistant/components/darksky/* @fabaff
|
||||
homeassistant/components/deconz/* @kane610
|
||||
homeassistant/components/demo/* @home-assistant/core
|
||||
homeassistant/components/digital_ocean/* @fabaff
|
||||
homeassistant/components/discogs/* @thibmaek
|
||||
homeassistant/components/doorbird/* @oblogic7
|
||||
homeassistant/components/dweet/* @fabaff
|
||||
|
||||
# E
|
||||
homeassistant/components/ecovacs/* @OverloadUT
|
||||
homeassistant/components/edp_redy/* @abmantis
|
||||
homeassistant/components/eight_sleep/* @mezz64
|
||||
homeassistant/components/egardia/* @jeroenterheerdt
|
||||
homeassistant/components/esphome/*.py @OttoWinter
|
||||
|
||||
# F
|
||||
homeassistant/components/freebox/*.py @snoof85
|
||||
|
||||
# G
|
||||
homeassistant/components/eight_sleep/* @mezz64
|
||||
homeassistant/components/emby/* @mezz64
|
||||
homeassistant/components/enigma2/* @fbradyirl
|
||||
homeassistant/components/ephember/* @ttroy50
|
||||
homeassistant/components/epsonworkforce/* @ThaStealth
|
||||
homeassistant/components/eq3btsmart/* @rytilahti
|
||||
homeassistant/components/esphome/* @OttoWinter
|
||||
homeassistant/components/file/* @fabaff
|
||||
homeassistant/components/filter/* @dgomes
|
||||
homeassistant/components/fitbit/* @robbiet480
|
||||
homeassistant/components/fixer/* @fabaff
|
||||
homeassistant/components/flock/* @fabaff
|
||||
homeassistant/components/flunearyou/* @bachya
|
||||
homeassistant/components/foursquare/* @robbiet480
|
||||
homeassistant/components/freebox/* @snoof85
|
||||
homeassistant/components/frontend/* @home-assistant/core
|
||||
homeassistant/components/gearbest/* @HerrHofrat
|
||||
homeassistant/components/gitter/* @fabaff
|
||||
homeassistant/components/glances/* @fabaff
|
||||
homeassistant/components/gntp/* @robbiet480
|
||||
homeassistant/components/google_translate/* @awarecan
|
||||
homeassistant/components/google_travel_time/* @robbiet480
|
||||
homeassistant/components/googlehome/* @ludeeus
|
||||
|
||||
# H
|
||||
homeassistant/components/gpsd/* @fabaff
|
||||
homeassistant/components/group/* @home-assistant/core
|
||||
homeassistant/components/gtfs/* @robbiet480
|
||||
homeassistant/components/harmony/* @ehendrix23
|
||||
homeassistant/components/hassio/* @home-assistant/hass-io
|
||||
homeassistant/components/heos/* @andrewsayre
|
||||
homeassistant/components/hikvision/* @mezz64
|
||||
homeassistant/components/hikvisioncam/* @fbradyirl
|
||||
homeassistant/components/history/* @home-assistant/core
|
||||
homeassistant/components/history_graph/* @andrey-git
|
||||
homeassistant/components/hive/* @Rendili @KJonline
|
||||
homeassistant/components/homeassistant/* @home-assistant/core
|
||||
homeassistant/components/homekit/* @cdce8p
|
||||
homeassistant/components/homematic/* @pvizeli @danielperna84
|
||||
homeassistant/components/html5/* @robbiet480
|
||||
homeassistant/components/http/* @home-assistant/core
|
||||
homeassistant/components/huawei_lte/* @scop
|
||||
|
||||
# I
|
||||
homeassistant/components/influx/* @fabaff
|
||||
homeassistant/components/huawei_router/* @abmantis
|
||||
homeassistant/components/hue/* @balloob
|
||||
homeassistant/components/ign_sismologia/* @exxamalte
|
||||
homeassistant/components/influxdb/* @fabaff
|
||||
homeassistant/components/input_boolean/* @home-assistant/core
|
||||
homeassistant/components/input_datetime/* @home-assistant/core
|
||||
homeassistant/components/input_number/* @home-assistant/core
|
||||
homeassistant/components/input_select/* @home-assistant/core
|
||||
homeassistant/components/input_text/* @home-assistant/core
|
||||
homeassistant/components/integration/* @dgomes
|
||||
homeassistant/components/ios/* @robbiet480
|
||||
homeassistant/components/ipma/* @dgomes
|
||||
|
||||
# K
|
||||
homeassistant/components/irish_rail_transport/* @ttroy50
|
||||
homeassistant/components/jewish_calendar/* @tsvi
|
||||
homeassistant/components/knx/* @Julius2342
|
||||
homeassistant/components/kodi/* @armills
|
||||
homeassistant/components/konnected/* @heythisisnate
|
||||
|
||||
# L
|
||||
homeassistant/components/lametric/* @robbiet480
|
||||
homeassistant/components/launch_library/* @ludeeus
|
||||
homeassistant/components/lifx/* @amelchio
|
||||
homeassistant/components/lifx_cloud/* @amelchio
|
||||
homeassistant/components/lifx_legacy/* @amelchio
|
||||
homeassistant/components/linux_battery/* @fabaff
|
||||
homeassistant/components/liveboxplaytv/* @pschmitt
|
||||
homeassistant/components/logger/* @home-assistant/core
|
||||
homeassistant/components/logi_circle/* @evanjd
|
||||
homeassistant/components/lovelace/* @home-assistant/core
|
||||
homeassistant/components/luci/* @fbradyirl
|
||||
homeassistant/components/luftdaten/* @fabaff
|
||||
|
||||
# M
|
||||
homeassistant/components/mastodon/* @fabaff
|
||||
homeassistant/components/matrix/* @tinloaf
|
||||
homeassistant/components/mediaroom/* @dgomes
|
||||
homeassistant/components/melissa/* @kennedyshead
|
||||
homeassistant/components/*/melissa.py @kennedyshead
|
||||
homeassistant/components/*/mystrom.py @fabaff
|
||||
|
||||
# N
|
||||
homeassistant/components/met/* @danielhiversen
|
||||
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/mill/* @danielhiversen
|
||||
homeassistant/components/min_max/* @fabaff
|
||||
homeassistant/components/mobile_app/* @robbiet480
|
||||
homeassistant/components/monoprice/* @etsinko
|
||||
homeassistant/components/moon/* @fabaff
|
||||
homeassistant/components/mpd/* @fabaff
|
||||
homeassistant/components/mqtt/* @home-assistant/core
|
||||
homeassistant/components/mystrom/* @fabaff
|
||||
homeassistant/components/nello/* @pschmitt
|
||||
homeassistant/components/ness_alarm/* @nickw444
|
||||
homeassistant/components/*/ness_alarm.py @nickw444
|
||||
homeassistant/components/nest/* @awarecan
|
||||
homeassistant/components/netdata/* @fabaff
|
||||
homeassistant/components/nissan_leaf/* @filcole
|
||||
homeassistant/components/nmbs/* @thibmaek
|
||||
homeassistant/components/no_ip/* @fabaff
|
||||
|
||||
# O
|
||||
homeassistant/components/notify/* @flowolf
|
||||
homeassistant/components/nsw_fuel_station/* @nickw444
|
||||
homeassistant/components/nuki/* @pschmitt
|
||||
homeassistant/components/ohmconnect/* @robbiet480
|
||||
homeassistant/components/onboarding/* @home-assistant/core
|
||||
homeassistant/components/openuv/* @bachya
|
||||
|
||||
# P
|
||||
homeassistant/components/openweathermap/* @fabaff
|
||||
homeassistant/components/owlet/* @oblogic7
|
||||
homeassistant/components/panel_custom/* @home-assistant/core
|
||||
homeassistant/components/panel_iframe/* @home-assistant/core
|
||||
homeassistant/components/persistent_notification/* @home-assistant/core
|
||||
homeassistant/components/pi_hole/* @fabaff
|
||||
homeassistant/components/plant/* @ChristianKuehnel
|
||||
homeassistant/components/point/* @fredrike
|
||||
|
||||
# Q
|
||||
homeassistant/components/pollen/* @bachya
|
||||
homeassistant/components/push/* @dgomes
|
||||
homeassistant/components/pvoutput/* @fabaff
|
||||
homeassistant/components/qnap/* @colinodell
|
||||
homeassistant/components/quantum_gateway/* @cisasteelersfan
|
||||
homeassistant/components/qwikswitch/* @kellerza
|
||||
|
||||
# R
|
||||
homeassistant/components/raincloud/* @vanstinator
|
||||
homeassistant/components/rainmachine/* @bachya
|
||||
homeassistant/components/random/* @fabaff
|
||||
homeassistant/components/rfxtrx/* @danielhiversen
|
||||
homeassistant/components/*/random.py @fabaff
|
||||
|
||||
# S
|
||||
homeassistant/components/rmvtransport/* @cgtobi
|
||||
homeassistant/components/roomba/* @pschmitt
|
||||
homeassistant/components/ruter/* @ludeeus
|
||||
homeassistant/components/scene/* @home-assistant/core
|
||||
homeassistant/components/scrape/* @fabaff
|
||||
homeassistant/components/script/* @home-assistant/core
|
||||
homeassistant/components/sensibo/* @andrey-git
|
||||
homeassistant/components/serial/* @fabaff
|
||||
homeassistant/components/seventeentrack/* @bachya
|
||||
homeassistant/components/shell_command/* @home-assistant/core
|
||||
homeassistant/components/shiftr/* @fabaff
|
||||
homeassistant/components/shodan/* @fabaff
|
||||
homeassistant/components/simplisafe/* @bachya
|
||||
homeassistant/components/sma/* @kellerza
|
||||
homeassistant/components/smartthings/* @andrewsayre
|
||||
homeassistant/components/smtp/* @fabaff
|
||||
homeassistant/components/sonos/* @amelchio
|
||||
homeassistant/components/spaceapi/* @fabaff
|
||||
homeassistant/components/spider/* @peternijssen
|
||||
|
||||
# T
|
||||
homeassistant/components/sql/* @dgomes
|
||||
homeassistant/components/statistics/* @fabaff
|
||||
homeassistant/components/stiebel_eltron/* @fucm
|
||||
homeassistant/components/sun/* @home-assistant/core
|
||||
homeassistant/components/supla/* @mwegrzynek
|
||||
homeassistant/components/swiss_hydrological_data/* @fabaff
|
||||
homeassistant/components/swiss_public_transport/* @fabaff
|
||||
homeassistant/components/switchbot/* @danielhiversen
|
||||
homeassistant/components/switchmate/* @danielhiversen
|
||||
homeassistant/components/synology_srm/* @aerialls
|
||||
homeassistant/components/syslog/* @fabaff
|
||||
homeassistant/components/sytadin/* @gautric
|
||||
homeassistant/components/tahoma/* @philklei
|
||||
homeassistant/components/tellduslive/*.py @fredrike
|
||||
homeassistant/components/tautulli/* @ludeeus
|
||||
homeassistant/components/tellduslive/* @fredrike
|
||||
homeassistant/components/template/* @PhracturedBlue
|
||||
homeassistant/components/tesla/* @zabuldon
|
||||
homeassistant/components/tfiac/* @fredrike @mellado
|
||||
homeassistant/components/thethingsnetwork/* @fabaff
|
||||
homeassistant/components/threshold/* @fabaff
|
||||
homeassistant/components/tibber/* @danielhiversen
|
||||
homeassistant/components/tplink/* @rytilahti
|
||||
homeassistant/components/tradfri/* @ggravlingen
|
||||
homeassistant/components/tile/* @bachya
|
||||
homeassistant/components/time_date/* @fabaff
|
||||
homeassistant/components/toon/* @frenck
|
||||
|
||||
# U
|
||||
homeassistant/components/tplink/* @rytilahti
|
||||
homeassistant/components/traccar/* @ludeeus
|
||||
homeassistant/components/tradfri/* @ggravlingen
|
||||
homeassistant/components/tts/* @robbiet480
|
||||
homeassistant/components/twilio_call/* @robbiet480
|
||||
homeassistant/components/twilio_sms/* @robbiet480
|
||||
homeassistant/components/uber/* @robbiet480
|
||||
homeassistant/components/unifi/* @kane610
|
||||
homeassistant/components/upcloud/* @scop
|
||||
homeassistant/components/updater/* @home-assistant/core
|
||||
homeassistant/components/upnp/* @robbiet480
|
||||
homeassistant/components/uptimerobot/* @ludeeus
|
||||
homeassistant/components/utility_meter/* @dgomes
|
||||
|
||||
# V
|
||||
homeassistant/components/velux/* @Julius2342
|
||||
|
||||
# W
|
||||
homeassistant/components/version/* @fabaff
|
||||
homeassistant/components/waqi/* @andrey-git
|
||||
homeassistant/components/weather/* @fabaff
|
||||
homeassistant/components/weblink/* @home-assistant/core
|
||||
homeassistant/components/websocket_api/* @home-assistant/core
|
||||
homeassistant/components/wemo/* @sqldiablo
|
||||
|
||||
# X
|
||||
homeassistant/components/worldclock/* @fabaff
|
||||
homeassistant/components/xfinity/* @cisasteelersfan
|
||||
homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
|
||||
homeassistant/components/xiaomi_miio/* @rytilahti @syssi
|
||||
|
||||
# Z
|
||||
homeassistant/components/xiaomi_tv/* @fattdev
|
||||
homeassistant/components/xmpp/* @fabaff
|
||||
homeassistant/components/yamaha_musiccast/* @jalmeroth
|
||||
homeassistant/components/yeelight/* @rytilahti @zewelor
|
||||
homeassistant/components/yeelightsunflower/* @lindsaymarkward
|
||||
homeassistant/components/yessssms/* @flowolf
|
||||
homeassistant/components/yi/* @bachya
|
||||
homeassistant/components/zeroconf/* @robbiet480
|
||||
homeassistant/components/zha/* @dmulcahey @adminiuga
|
||||
homeassistant/components/zone/* @home-assistant/core
|
||||
homeassistant/components/zoneminder/* @rohankapoorcom
|
||||
homeassistant/components/zwave/* @home-assistant/z-wave
|
||||
|
||||
# Other code
|
||||
homeassistant/scripts/check_config.py @kellerza
|
||||
# Individual files
|
||||
homeassistant/components/group/cover @cdce8p
|
||||
homeassistant/components/demo/weather @fabaff
|
||||
|
||||
@@ -27,7 +27,7 @@ COPY requirements_all.txt requirements_all.txt
|
||||
# Uninstall enum34 because some dependencies install it but breaks Python 3.4+.
|
||||
# See PR #8103 for more info.
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
|
||||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop==0.11.3 cchardet cython tensorflow
|
||||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop==0.12.2 cchardet cython tensorflow
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Home Assistant |Build Status| |Coverage Status| |Chat Status|
|
||||
Home Assistant |Build Status| |CI 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.
|
||||
@@ -27,8 +27,10 @@ components <https://developers.home-assistant.io/docs/en/creating_component_inde
|
||||
If you run into issues while using Home Assistant or during development
|
||||
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
|
||||
|
||||
.. |Build Status| image:: https://travis-ci.org/home-assistant/home-assistant.svg?branch=master
|
||||
.. |Build Status| image:: https://travis-ci.org/home-assistant/home-assistant.svg?branch=dev
|
||||
:target: https://travis-ci.org/home-assistant/home-assistant
|
||||
.. |CI Status| image:: https://circleci.com/gh/home-assistant/home-assistant.svg?style=shield
|
||||
:target: https://circleci.com/gh/home-assistant/home-assistant
|
||||
.. |Coverage Status| image:: https://img.shields.io/coveralls/home-assistant/home-assistant.svg
|
||||
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
|
||||
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
|
||||
|
||||
@@ -18,8 +18,26 @@ from ..models import Credentials, UserMeta
|
||||
IPAddress = Union[IPv4Address, IPv6Address]
|
||||
IPNetwork = Union[IPv4Network, IPv6Network]
|
||||
|
||||
CONF_TRUSTED_NETWORKS = 'trusted_networks'
|
||||
CONF_TRUSTED_USERS = 'trusted_users'
|
||||
CONF_GROUP = 'group'
|
||||
CONF_ALLOW_BYPASS_LOGIN = 'allow_bypass_login'
|
||||
|
||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
|
||||
vol.Required('trusted_networks'): vol.All(cv.ensure_list, [ip_network])
|
||||
vol.Required(CONF_TRUSTED_NETWORKS): vol.All(
|
||||
cv.ensure_list, [ip_network]
|
||||
),
|
||||
vol.Optional(CONF_TRUSTED_USERS, default={}): vol.Schema(
|
||||
# we only validate the format of user_id or group_id
|
||||
{ip_network: vol.All(
|
||||
cv.ensure_list,
|
||||
[vol.Or(
|
||||
cv.uuid4_hex,
|
||||
vol.Schema({vol.Required(CONF_GROUP): cv.uuid4_hex}),
|
||||
)],
|
||||
)}
|
||||
),
|
||||
vol.Optional(CONF_ALLOW_BYPASS_LOGIN, default=False): cv.boolean,
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
|
||||
@@ -43,7 +61,12 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||
@property
|
||||
def trusted_networks(self) -> List[IPNetwork]:
|
||||
"""Return trusted networks."""
|
||||
return cast(List[IPNetwork], self.config['trusted_networks'])
|
||||
return cast(List[IPNetwork], self.config[CONF_TRUSTED_NETWORKS])
|
||||
|
||||
@property
|
||||
def trusted_users(self) -> Dict[IPNetwork, Any]:
|
||||
"""Return trusted users per network."""
|
||||
return cast(Dict[IPNetwork, Any], self.config[CONF_TRUSTED_USERS])
|
||||
|
||||
@property
|
||||
def support_mfa(self) -> bool:
|
||||
@@ -53,13 +76,34 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
||||
"""Return a flow to login."""
|
||||
assert context is not None
|
||||
ip_addr = cast(IPAddress, context.get('ip_address'))
|
||||
users = await self.store.async_get_users()
|
||||
available_users = {user.id: user.name
|
||||
for user in users
|
||||
if not user.system_generated and user.is_active}
|
||||
available_users = [user for user in users
|
||||
if not user.system_generated and user.is_active]
|
||||
for ip_net, user_or_group_list in self.trusted_users.items():
|
||||
if ip_addr in ip_net:
|
||||
user_list = [user_id for user_id in user_or_group_list
|
||||
if isinstance(user_id, str)]
|
||||
group_list = [group[CONF_GROUP] for group in user_or_group_list
|
||||
if isinstance(group, dict)]
|
||||
flattened_group_list = [group for sublist in group_list
|
||||
for group in sublist]
|
||||
available_users = [
|
||||
user for user in available_users
|
||||
if (user.id in user_list or
|
||||
any([group.id in flattened_group_list
|
||||
for group in user.groups]))
|
||||
]
|
||||
break
|
||||
|
||||
return TrustedNetworksLoginFlow(
|
||||
self, cast(IPAddress, context.get('ip_address')), available_users)
|
||||
self,
|
||||
ip_addr,
|
||||
{
|
||||
user.id: user.name for user in available_users
|
||||
},
|
||||
self.config[CONF_ALLOW_BYPASS_LOGIN],
|
||||
)
|
||||
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]) -> Credentials:
|
||||
@@ -109,11 +153,13 @@ class TrustedNetworksLoginFlow(LoginFlow):
|
||||
|
||||
def __init__(self, auth_provider: TrustedNetworksAuthProvider,
|
||||
ip_addr: IPAddress,
|
||||
available_users: Dict[str, Optional[str]]) -> None:
|
||||
available_users: Dict[str, Optional[str]],
|
||||
allow_bypass_login: bool) -> None:
|
||||
"""Initialize the login flow."""
|
||||
super().__init__(auth_provider)
|
||||
self._available_users = available_users
|
||||
self._ip_address = ip_addr
|
||||
self._allow_bypass_login = allow_bypass_login
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
@@ -131,6 +177,11 @@ class TrustedNetworksLoginFlow(LoginFlow):
|
||||
if user_input is not None:
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
if self._allow_bypass_login and len(self._available_users) == 1:
|
||||
return await self.async_finish({
|
||||
'user': next(iter(self._available_users.keys()))
|
||||
})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema({'user': vol.In(self._available_users)}),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Provide methods to bootstrap a Home Assistant instance."""
|
||||
import asyncio
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
@@ -9,10 +10,7 @@ from typing import Any, Optional, Dict, Set
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import (
|
||||
core, config as conf_util, config_entries, components as core_components,
|
||||
loader)
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant import core, config as conf_util, config_entries, loader
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.logging import AsyncHandler
|
||||
@@ -28,50 +26,16 @@ ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
# hass.data key for logging information.
|
||||
DATA_LOGGING = 'logging'
|
||||
|
||||
LOGGING_COMPONENT = {'logger', 'system_log'}
|
||||
|
||||
FIRST_INIT_COMPONENT = {
|
||||
CORE_INTEGRATIONS = ('homeassistant', 'persistent_notification')
|
||||
LOGGING_INTEGRATIONS = {'logger', 'system_log'}
|
||||
STAGE_1_INTEGRATIONS = {
|
||||
# To record data
|
||||
'recorder',
|
||||
'mqtt',
|
||||
# To make sure we forward data to other instances
|
||||
'mqtt_eventstream',
|
||||
'introduction',
|
||||
'frontend',
|
||||
'history',
|
||||
}
|
||||
|
||||
|
||||
def from_config_dict(config: Dict[str, Any],
|
||||
hass: Optional[core.HomeAssistant] = None,
|
||||
config_dir: Optional[str] = None,
|
||||
enable_log: bool = True,
|
||||
verbose: bool = False,
|
||||
skip_pip: bool = False,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a configuration dictionary.
|
||||
|
||||
Dynamically loads required components and its dependencies.
|
||||
"""
|
||||
if hass is None:
|
||||
hass = core.HomeAssistant()
|
||||
if config_dir is not None:
|
||||
config_dir = os.path.abspath(config_dir)
|
||||
hass.config.config_dir = config_dir
|
||||
if not is_virtual_env():
|
||||
hass.loop.run_until_complete(
|
||||
async_mount_local_lib_path(config_dir))
|
||||
|
||||
# run task
|
||||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_dict(
|
||||
config, hass, config_dir, enable_log, verbose, skip_pip,
|
||||
log_rotate_days, log_file, log_no_color)
|
||||
)
|
||||
return hass
|
||||
|
||||
|
||||
async def async_from_config_dict(config: Dict[str, Any],
|
||||
hass: core.HomeAssistant,
|
||||
config_dir: Optional[str] = None,
|
||||
@@ -114,64 +78,17 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
"Further initialization aborted")
|
||||
return None
|
||||
|
||||
await hass.async_add_executor_job(
|
||||
conf_util.process_ha_config_upgrade, hass)
|
||||
|
||||
# Make a copy because we are mutating it.
|
||||
config = OrderedDict(config)
|
||||
|
||||
# Merge packages
|
||||
conf_util.merge_packages_config(
|
||||
await conf_util.merge_packages_config(
|
||||
hass, config, core_config.get(conf_util.CONF_PACKAGES, {}))
|
||||
|
||||
hass.config_entries = config_entries.ConfigEntries(hass, config)
|
||||
await hass.config_entries.async_initialize()
|
||||
|
||||
components = _get_components(hass, config)
|
||||
|
||||
# Resolve all dependencies of all components.
|
||||
for component in list(components):
|
||||
try:
|
||||
components.update(loader.component_dependencies(hass, component))
|
||||
except loader.LoaderError:
|
||||
# Ignore it, or we'll break startup
|
||||
# It will be properly handled during setup.
|
||||
pass
|
||||
|
||||
# setup components
|
||||
res = await core_components.async_setup(hass, config)
|
||||
if not res:
|
||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
||||
"Further initialization aborted")
|
||||
return hass
|
||||
|
||||
await persistent_notification.async_setup(hass, config)
|
||||
|
||||
_LOGGER.info("Home Assistant core initialized")
|
||||
|
||||
# stage 0, load logging components
|
||||
for component in components:
|
||||
if component in LOGGING_COMPONENT:
|
||||
hass.async_create_task(
|
||||
async_setup_component(hass, component, config))
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# stage 1
|
||||
for component in components:
|
||||
if component in FIRST_INIT_COMPONENT:
|
||||
hass.async_create_task(
|
||||
async_setup_component(hass, component, config))
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# stage 2
|
||||
for component in components:
|
||||
if component in FIRST_INIT_COMPONENT or component in LOGGING_COMPONENT:
|
||||
continue
|
||||
hass.async_create_task(async_setup_component(hass, component, config))
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await _async_set_up_integrations(hass, config)
|
||||
|
||||
stop = time()
|
||||
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
|
||||
@@ -224,32 +141,6 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
return hass
|
||||
|
||||
|
||||
def from_config_file(config_path: str,
|
||||
hass: Optional[core.HomeAssistant] = None,
|
||||
verbose: bool = False,
|
||||
skip_pip: bool = True,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False)\
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter if given,
|
||||
instantiates a new Home Assistant object if 'hass' is not given.
|
||||
"""
|
||||
if hass is None:
|
||||
hass = core.HomeAssistant()
|
||||
|
||||
# run task
|
||||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_file(
|
||||
config_path, hass, verbose, skip_pip,
|
||||
log_rotate_days, log_file, log_no_color)
|
||||
)
|
||||
|
||||
return hass
|
||||
|
||||
|
||||
async def async_from_config_file(config_path: str,
|
||||
hass: core.HomeAssistant,
|
||||
verbose: bool = False,
|
||||
@@ -273,6 +164,9 @@ async def async_from_config_file(config_path: str,
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file,
|
||||
log_no_color)
|
||||
|
||||
await hass.async_add_executor_job(
|
||||
conf_util.process_ha_config_upgrade, hass)
|
||||
|
||||
try:
|
||||
config_dict = await hass.async_add_executor_job(
|
||||
conf_util.load_yaml_config_file, config_path)
|
||||
@@ -391,18 +285,127 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
|
||||
|
||||
|
||||
@core.callback
|
||||
def _get_components(hass: core.HomeAssistant,
|
||||
config: Dict[str, Any]) -> Set[str]:
|
||||
"""Get components to set up."""
|
||||
def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]:
|
||||
"""Get domains of components to set up."""
|
||||
# Filter out the repeating and common config section [homeassistant]
|
||||
components = set(key.split(' ')[0] for key in config.keys()
|
||||
if key != core.DOMAIN)
|
||||
domains = set(key.split(' ')[0] for key in config.keys()
|
||||
if key != core.DOMAIN)
|
||||
|
||||
# Add config entry domains
|
||||
components.update(hass.config_entries.async_domains()) # type: ignore
|
||||
domains.update(hass.config_entries.async_domains()) # type: ignore
|
||||
|
||||
# Make sure the Hass.io component is loaded
|
||||
if 'HASSIO' in os.environ:
|
||||
components.add('hassio')
|
||||
domains.add('hassio')
|
||||
|
||||
return components
|
||||
return domains
|
||||
|
||||
|
||||
async def _async_set_up_integrations(
|
||||
hass: core.HomeAssistant, config: Dict[str, Any]) -> None:
|
||||
"""Set up all the integrations."""
|
||||
domains = _get_domains(hass, config)
|
||||
|
||||
# Resolve all dependencies of all components so we can find the logging
|
||||
# and integrations that need faster initialization.
|
||||
resolved_domains_task = asyncio.gather(*[
|
||||
loader.async_component_dependencies(hass, domain)
|
||||
for domain in domains
|
||||
], return_exceptions=True)
|
||||
|
||||
# Set up core.
|
||||
_LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)
|
||||
|
||||
if not all(await asyncio.gather(*[
|
||||
async_setup_component(hass, domain, config)
|
||||
for domain in CORE_INTEGRATIONS
|
||||
])):
|
||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
||||
"Further initialization aborted")
|
||||
return
|
||||
|
||||
_LOGGER.debug("Home Assistant core initialized")
|
||||
|
||||
# Finish resolving domains
|
||||
for dep_domains in await resolved_domains_task:
|
||||
# Result is either a set or an exception. We ignore exceptions
|
||||
# It will be properly handled during setup of the domain.
|
||||
if isinstance(dep_domains, set):
|
||||
domains.update(dep_domains)
|
||||
|
||||
# setup components
|
||||
logging_domains = domains & LOGGING_INTEGRATIONS
|
||||
stage_1_domains = domains & STAGE_1_INTEGRATIONS
|
||||
stage_2_domains = domains - logging_domains - stage_1_domains
|
||||
|
||||
if logging_domains:
|
||||
_LOGGER.debug("Setting up %s", logging_domains)
|
||||
|
||||
await asyncio.gather(*[
|
||||
async_setup_component(hass, domain, config)
|
||||
for domain in logging_domains
|
||||
])
|
||||
|
||||
# Kick off loading the registries. They don't need to be awaited.
|
||||
asyncio.gather(
|
||||
hass.helpers.device_registry.async_get_registry(),
|
||||
hass.helpers.entity_registry.async_get_registry(),
|
||||
hass.helpers.area_registry.async_get_registry())
|
||||
|
||||
if stage_1_domains:
|
||||
await asyncio.gather(*[
|
||||
async_setup_component(hass, domain, config)
|
||||
for domain in stage_1_domains
|
||||
])
|
||||
|
||||
# Load all integrations
|
||||
after_dependencies = {} # type: Dict[str, Set[str]]
|
||||
|
||||
for int_or_exc in await asyncio.gather(*[
|
||||
loader.async_get_integration(hass, domain)
|
||||
for domain in stage_2_domains
|
||||
], return_exceptions=True):
|
||||
# Exceptions are handled in async_setup_component.
|
||||
if (isinstance(int_or_exc, loader.Integration) and
|
||||
int_or_exc.after_dependencies):
|
||||
after_dependencies[int_or_exc.domain] = set(
|
||||
int_or_exc.after_dependencies
|
||||
)
|
||||
|
||||
last_load = None
|
||||
while stage_2_domains:
|
||||
domains_to_load = set()
|
||||
|
||||
for domain in stage_2_domains:
|
||||
after_deps = after_dependencies.get(domain)
|
||||
# Load if integration has no after_dependencies or they are
|
||||
# all loaded
|
||||
if (not after_deps or
|
||||
not after_deps-hass.config.components):
|
||||
domains_to_load.add(domain)
|
||||
|
||||
if not domains_to_load or domains_to_load == last_load:
|
||||
break
|
||||
|
||||
_LOGGER.debug("Setting up %s", domains_to_load)
|
||||
|
||||
await asyncio.gather(*[
|
||||
async_setup_component(hass, domain, config)
|
||||
for domain in domains_to_load
|
||||
])
|
||||
|
||||
last_load = domains_to_load
|
||||
stage_2_domains -= domains_to_load
|
||||
|
||||
# These are stage 2 domains that never have their after_dependencies
|
||||
# satisfied.
|
||||
if stage_2_domains:
|
||||
_LOGGER.debug("Final set up: %s", stage_2_domains)
|
||||
|
||||
await asyncio.gather(*[
|
||||
async_setup_component(hass, domain, config)
|
||||
for domain in stage_2_domains
|
||||
])
|
||||
|
||||
# Wrap up startup
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -7,33 +7,12 @@ Component design guidelines:
|
||||
format "<DOMAIN>.<OBJECT_ID>".
|
||||
- Each component should publish services only under its own domain.
|
||||
"""
|
||||
import asyncio
|
||||
import itertools as it
|
||||
import logging
|
||||
from typing import Awaitable
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.config as conf_util
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.service import async_extract_entity_ids
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
|
||||
SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART,
|
||||
RESTART_EXIT_CODE)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.core import split_entity_id
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config'
|
||||
SERVICE_CHECK_CONFIG = 'check_config'
|
||||
SERVICE_UPDATE_ENTITY = 'update_entity'
|
||||
SCHEMA_UPDATE_ENTITY = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_ids
|
||||
})
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
"""Load up the module to call the is_on method.
|
||||
@@ -46,7 +25,7 @@ def is_on(hass, entity_id=None):
|
||||
entity_ids = hass.states.entity_ids()
|
||||
|
||||
for ent_id in entity_ids:
|
||||
domain = ha.split_entity_id(ent_id)[0]
|
||||
domain = split_entity_id(ent_id)[0]
|
||||
|
||||
try:
|
||||
component = getattr(hass.components, domain)
|
||||
@@ -64,113 +43,3 @@ def is_on(hass, entity_id=None):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
||||
"""Set up general services related to Home Assistant."""
|
||||
async def async_handle_turn_service(service):
|
||||
"""Handle calls to homeassistant.turn_on/off."""
|
||||
entity_ids = await async_extract_entity_ids(hass, service)
|
||||
|
||||
# Generic turn on/off method requires entity id
|
||||
if not entity_ids:
|
||||
_LOGGER.error(
|
||||
"homeassistant/%s cannot be called without entity_id",
|
||||
service.service)
|
||||
return
|
||||
|
||||
# Group entity_ids by domain. groupby requires sorted data.
|
||||
by_domain = it.groupby(sorted(entity_ids),
|
||||
lambda item: ha.split_entity_id(item)[0])
|
||||
|
||||
tasks = []
|
||||
|
||||
for domain, ent_ids in by_domain:
|
||||
# We want to block for all calls and only return when all calls
|
||||
# have been processed. If a service does not exist it causes a 10
|
||||
# second delay while we're blocking waiting for a response.
|
||||
# But services can be registered on other HA instances that are
|
||||
# listening to the bus too. So as an in between solution, we'll
|
||||
# block only if the service is defined in the current HA instance.
|
||||
blocking = hass.services.has_service(domain, service.service)
|
||||
|
||||
# Create a new dict for this call
|
||||
data = dict(service.data)
|
||||
|
||||
# ent_ids is a generator, convert it to a list.
|
||||
data[ATTR_ENTITY_ID] = list(ent_ids)
|
||||
|
||||
tasks.append(hass.services.async_call(
|
||||
domain, service.service, data, blocking))
|
||||
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned {} on"))
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF,
|
||||
"Turned {} off"))
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}"))
|
||||
|
||||
async def async_handle_core_service(call):
|
||||
"""Service handler for handling core services."""
|
||||
if call.service == SERVICE_HOMEASSISTANT_STOP:
|
||||
hass.async_create_task(hass.async_stop())
|
||||
return
|
||||
|
||||
try:
|
||||
errors = await conf_util.async_check_ha_config_file(hass)
|
||||
except HomeAssistantError:
|
||||
return
|
||||
|
||||
if errors:
|
||||
_LOGGER.error(errors)
|
||||
hass.components.persistent_notification.async_create(
|
||||
"Config error. See dev-info panel for details.",
|
||||
"Config validating", "{0}.check_config".format(ha.DOMAIN))
|
||||
return
|
||||
|
||||
if call.service == SERVICE_HOMEASSISTANT_RESTART:
|
||||
hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE))
|
||||
|
||||
async def async_handle_update_service(call):
|
||||
"""Service handler for updating an entity."""
|
||||
tasks = [hass.helpers.entity_component.async_update_entity(entity)
|
||||
for entity in call.data[ATTR_ENTITY_ID]]
|
||||
|
||||
if tasks:
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_UPDATE_ENTITY, async_handle_update_service,
|
||||
schema=SCHEMA_UPDATE_ENTITY)
|
||||
|
||||
async def async_handle_reload_config(call):
|
||||
"""Service handler for reloading core config."""
|
||||
try:
|
||||
conf = await conf_util.async_hass_config_yaml(hass)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
# auth only processed during startup
|
||||
await conf_util.async_process_ha_core_config(
|
||||
hass, conf.get(ha.DOMAIN) or {})
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config)
|
||||
|
||||
return True
|
||||
|
||||
@@ -13,8 +13,6 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['abodepy==0.15.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTRIBUTION = "Data provided by goabode.com"
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.abode import ATTRIBUTION, AbodeDevice
|
||||
from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED)
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
from . import ATTRIBUTION, DOMAIN as ABODE_DOMAIN, AbodeDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
"""Support for Abode Security System binary sensors."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.abode import (AbodeDevice, AbodeAutomation,
|
||||
DOMAIN as ABODE_DOMAIN)
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from . import DOMAIN as ABODE_DOMAIN, AbodeAutomation, AbodeDevice
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
"""Support for Abode Security System cameras."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
import requests
|
||||
|
||||
from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
from . import DOMAIN as ABODE_DOMAIN, AbodeDevice
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""Support for Abode Security System covers."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
from . import DOMAIN as ABODE_DOMAIN, AbodeDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
"""Support for Abode Security System lights."""
|
||||
import logging
|
||||
from math import ceil
|
||||
from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_COLOR_TEMP,
|
||||
SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light)
|
||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light)
|
||||
from homeassistant.util.color import (
|
||||
color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin)
|
||||
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
from . import DOMAIN as ABODE_DOMAIN, AbodeDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""Support for Abode Security System locks."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN
|
||||
from homeassistant.components.lock import LockDevice
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
from . import DOMAIN as ABODE_DOMAIN, AbodeDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
10
homeassistant/components/abode/manifest.json
Normal file
10
homeassistant/components/abode/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "abode",
|
||||
"name": "Abode",
|
||||
"documentation": "https://www.home-assistant.io/components/abode",
|
||||
"requirements": [
|
||||
"abodepy==0.15.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
"""Support for Abode Security System sensors."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN
|
||||
from homeassistant.const import (
|
||||
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from . import DOMAIN as ABODE_DOMAIN, AbodeDevice
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Sensor types: Name, icon
|
||||
SENSOR_TYPES = {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
"""Support for Abode Security System switches."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.abode import (AbodeDevice, AbodeAutomation,
|
||||
DOMAIN as ABODE_DOMAIN)
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from . import DOMAIN as ABODE_DOMAIN, AbodeAutomation, AbodeDevice
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
1
homeassistant/components/acer_projector/__init__.py
Normal file
1
homeassistant/components/acer_projector/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""The acer_projector component."""
|
||||
10
homeassistant/components/acer_projector/manifest.json
Normal file
10
homeassistant/components/acer_projector/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "acer_projector",
|
||||
"name": "Acer projector",
|
||||
"documentation": "https://www.home-assistant.io/components/acer_projector",
|
||||
"requirements": [
|
||||
"pyserial==3.1.1"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
}
|
||||
161
homeassistant/components/acer_projector/switch.py
Normal file
161
homeassistant/components/acer_projector/switch.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""Use serial protocol of Acer projector to obtain state of the projector."""
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, STATE_UNKNOWN, CONF_NAME, CONF_FILENAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_TIMEOUT = 'timeout'
|
||||
CONF_WRITE_TIMEOUT = 'write_timeout'
|
||||
|
||||
DEFAULT_NAME = 'Acer Projector'
|
||||
DEFAULT_TIMEOUT = 1
|
||||
DEFAULT_WRITE_TIMEOUT = 1
|
||||
|
||||
ECO_MODE = 'ECO Mode'
|
||||
|
||||
ICON = 'mdi:projector'
|
||||
|
||||
INPUT_SOURCE = 'Input Source'
|
||||
|
||||
LAMP = 'Lamp'
|
||||
LAMP_HOURS = 'Lamp Hours'
|
||||
|
||||
MODEL = 'Model'
|
||||
|
||||
# Commands known to the projector
|
||||
CMD_DICT = {
|
||||
LAMP: '* 0 Lamp ?\r',
|
||||
LAMP_HOURS: '* 0 Lamp\r',
|
||||
INPUT_SOURCE: '* 0 Src ?\r',
|
||||
ECO_MODE: '* 0 IR 052\r',
|
||||
MODEL: '* 0 IR 035\r',
|
||||
STATE_ON: '* 0 IR 001\r',
|
||||
STATE_OFF: '* 0 IR 002\r',
|
||||
}
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_FILENAME): cv.isdevice,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_WRITE_TIMEOUT, default=DEFAULT_WRITE_TIMEOUT):
|
||||
cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Connect with serial port and return Acer Projector."""
|
||||
serial_port = config.get(CONF_FILENAME)
|
||||
name = config.get(CONF_NAME)
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
write_timeout = config.get(CONF_WRITE_TIMEOUT)
|
||||
|
||||
add_entities([AcerSwitch(serial_port, name, timeout, write_timeout)], True)
|
||||
|
||||
|
||||
class AcerSwitch(SwitchDevice):
|
||||
"""Represents an Acer Projector as a switch."""
|
||||
|
||||
def __init__(self, serial_port, name, timeout, write_timeout, **kwargs):
|
||||
"""Init of the Acer projector."""
|
||||
import serial
|
||||
self.ser = serial.Serial(
|
||||
port=serial_port, timeout=timeout, write_timeout=write_timeout,
|
||||
**kwargs)
|
||||
self._serial_port = serial_port
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._available = False
|
||||
self._attributes = {
|
||||
LAMP_HOURS: STATE_UNKNOWN,
|
||||
INPUT_SOURCE: STATE_UNKNOWN,
|
||||
ECO_MODE: STATE_UNKNOWN,
|
||||
}
|
||||
|
||||
def _write_read(self, msg):
|
||||
"""Write to the projector and read the return."""
|
||||
import serial
|
||||
ret = ""
|
||||
# Sometimes the projector won't answer for no reason or the projector
|
||||
# was disconnected during runtime.
|
||||
# This way the projector can be reconnected and will still work
|
||||
try:
|
||||
if not self.ser.is_open:
|
||||
self.ser.open()
|
||||
msg = msg.encode('utf-8')
|
||||
self.ser.write(msg)
|
||||
# Size is an experience value there is no real limit.
|
||||
# AFAIK there is no limit and no end character so we will usually
|
||||
# need to wait for timeout
|
||||
ret = self.ser.read_until(size=20).decode('utf-8')
|
||||
except serial.SerialException:
|
||||
_LOGGER.error('Problem communicating with %s', self._serial_port)
|
||||
self.ser.close()
|
||||
return ret
|
||||
|
||||
def _write_read_format(self, msg):
|
||||
"""Write msg, obtain answer and format output."""
|
||||
# answers are formatted as ***\answer\r***
|
||||
awns = self._write_read(msg)
|
||||
match = re.search(r'\r(.+)\r', awns)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if projector is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return name of the projector."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return if the projector is turned on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return state attributes."""
|
||||
return self._attributes
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state from the projector."""
|
||||
msg = CMD_DICT[LAMP]
|
||||
awns = self._write_read_format(msg)
|
||||
if awns == 'Lamp 1':
|
||||
self._state = True
|
||||
self._available = True
|
||||
elif awns == 'Lamp 0':
|
||||
self._state = False
|
||||
self._available = True
|
||||
else:
|
||||
self._available = False
|
||||
|
||||
for key in self._attributes:
|
||||
msg = CMD_DICT.get(key, None)
|
||||
if msg:
|
||||
awns = self._write_read_format(msg)
|
||||
self._attributes[key] = awns
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the projector on."""
|
||||
msg = CMD_DICT[STATE_ON]
|
||||
self._write_read(msg)
|
||||
self._state = STATE_ON
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the projector off."""
|
||||
msg = CMD_DICT[STATE_OFF]
|
||||
self._write_read(msg)
|
||||
self._state = STATE_OFF
|
||||
1
homeassistant/components/actiontec/__init__.py
Normal file
1
homeassistant/components/actiontec/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""The actiontec component."""
|
||||
115
homeassistant/components/actiontec/device_tracker.py
Normal file
115
homeassistant/components/actiontec/device_tracker.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Support for Actiontec MI424WR (Verizon FIOS) routers."""
|
||||
import logging
|
||||
import re
|
||||
import telnetlib
|
||||
from collections import namedtuple
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_LEASES_REGEX = re.compile(
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' +
|
||||
r'\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))' +
|
||||
r'\svalid\sfor:\s(?P<timevalid>(-?\d+))' +
|
||||
r'\ssec')
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string
|
||||
})
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return an Actiontec scanner."""
|
||||
scanner = ActiontecDeviceScanner(config[DOMAIN])
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
Device = namedtuple('Device', ['mac', 'ip', 'last_update'])
|
||||
|
||||
|
||||
class ActiontecDeviceScanner(DeviceScanner):
|
||||
"""This class queries an actiontec router for connected devices."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
self.last_results = []
|
||||
data = self.get_actiontec_data()
|
||||
self.success_init = data is not None
|
||||
_LOGGER.info("canner initialized")
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
return [client.mac for client in self.last_results]
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
if not self.last_results:
|
||||
return None
|
||||
for client in self.last_results:
|
||||
if client.mac == device:
|
||||
return client.ip
|
||||
return None
|
||||
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the router is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
_LOGGER.info("Scanning")
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
now = dt_util.now()
|
||||
actiontec_data = self.get_actiontec_data()
|
||||
if not actiontec_data:
|
||||
return False
|
||||
self.last_results = [Device(data['mac'], name, now)
|
||||
for name, data in actiontec_data.items()
|
||||
if data['timevalid'] > -60]
|
||||
_LOGGER.info("Scan successful")
|
||||
return True
|
||||
|
||||
def get_actiontec_data(self):
|
||||
"""Retrieve data from Actiontec MI424WR and return parsed result."""
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self.host)
|
||||
telnet.read_until(b'Username: ')
|
||||
telnet.write((self.username + '\n').encode('ascii'))
|
||||
telnet.read_until(b'Password: ')
|
||||
telnet.write((self.password + '\n').encode('ascii'))
|
||||
prompt = telnet.read_until(
|
||||
b'Wireless Broadband Router> ').split(b'\n')[-1]
|
||||
telnet.write('firewall mac_cache_dump\n'.encode('ascii'))
|
||||
telnet.write('\n'.encode('ascii'))
|
||||
telnet.read_until(prompt)
|
||||
leases_result = telnet.read_until(prompt).split(b'\n')[1:-1]
|
||||
telnet.write('exit\n'.encode('ascii'))
|
||||
except EOFError:
|
||||
_LOGGER.exception("Unexpected response from router")
|
||||
return
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.exception("Connection refused by router. Telnet enabled?")
|
||||
return None
|
||||
|
||||
devices = {}
|
||||
for lease in leases_result:
|
||||
match = _LEASES_REGEX.search(lease.decode('utf-8'))
|
||||
if match is not None:
|
||||
devices[match.group('ip')] = {
|
||||
'ip': match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
'timevalid': int(match.group('timevalid'))
|
||||
}
|
||||
return devices
|
||||
8
homeassistant/components/actiontec/manifest.json
Normal file
8
homeassistant/components/actiontec/manifest.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"domain": "actiontec",
|
||||
"name": "Actiontec",
|
||||
"documentation": "https://www.home-assistant.io/components/actiontec",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
}
|
||||
@@ -4,14 +4,15 @@ import struct
|
||||
import logging
|
||||
import ctypes
|
||||
from collections import namedtuple
|
||||
import asyncio
|
||||
import async_timeout
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE, CONF_IP_ADDRESS, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyads==3.0.7']
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,6 +32,9 @@ CONF_ADS_VALUE = 'value'
|
||||
CONF_ADS_VAR = 'adsvar'
|
||||
CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness'
|
||||
|
||||
STATE_KEY_STATE = 'state'
|
||||
STATE_KEY_BRIGHTNESS = 'brightness'
|
||||
|
||||
DOMAIN = 'ads'
|
||||
|
||||
SERVICE_WRITE_DATA_BY_NAME = 'write_data_by_name'
|
||||
@@ -154,28 +158,41 @@ class AdsHub:
|
||||
|
||||
def write_by_name(self, name, value, plc_datatype):
|
||||
"""Write a value to the device."""
|
||||
import pyads
|
||||
with self._lock:
|
||||
return self._client.write_by_name(name, value, plc_datatype)
|
||||
try:
|
||||
return self._client.write_by_name(name, value, plc_datatype)
|
||||
except pyads.ADSError as err:
|
||||
_LOGGER.error("Error writing %s: %s", name, err)
|
||||
|
||||
def read_by_name(self, name, plc_datatype):
|
||||
"""Read a value from the device."""
|
||||
import pyads
|
||||
with self._lock:
|
||||
return self._client.read_by_name(name, plc_datatype)
|
||||
try:
|
||||
return self._client.read_by_name(name, plc_datatype)
|
||||
except pyads.ADSError as err:
|
||||
_LOGGER.error("Error reading %s: %s", name, err)
|
||||
|
||||
def add_device_notification(self, name, plc_datatype, callback):
|
||||
"""Add a notification to the ADS devices."""
|
||||
from pyads import NotificationAttrib
|
||||
attr = NotificationAttrib(ctypes.sizeof(plc_datatype))
|
||||
import pyads
|
||||
attr = pyads.NotificationAttrib(ctypes.sizeof(plc_datatype))
|
||||
|
||||
with self._lock:
|
||||
hnotify, huser = self._client.add_device_notification(
|
||||
name, attr, self._device_notification_callback)
|
||||
hnotify = int(hnotify)
|
||||
self._notification_items[hnotify] = NotificationItem(
|
||||
hnotify, huser, name, plc_datatype, callback)
|
||||
try:
|
||||
hnotify, huser = self._client.add_device_notification(
|
||||
name, attr, self._device_notification_callback)
|
||||
except pyads.ADSError as err:
|
||||
_LOGGER.error("Error subscribing to %s: %s", name, err)
|
||||
else:
|
||||
hnotify = int(hnotify)
|
||||
self._notification_items[hnotify] = NotificationItem(
|
||||
hnotify, huser, name, plc_datatype, callback)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Added device notification %d for variable %s", hnotify, name)
|
||||
_LOGGER.debug(
|
||||
"Added device notification %d for variable %s",
|
||||
hnotify, name)
|
||||
|
||||
def _device_notification_callback(self, notification, name):
|
||||
"""Handle device notifications."""
|
||||
@@ -210,3 +227,68 @@ class AdsHub:
|
||||
_LOGGER.warning("No callback available for this datatype")
|
||||
|
||||
notification_item.callback(notification_item.name, value)
|
||||
|
||||
|
||||
class AdsEntity(Entity):
|
||||
"""Representation of ADS entity."""
|
||||
|
||||
def __init__(self, ads_hub, name, ads_var):
|
||||
"""Initialize ADS binary sensor."""
|
||||
self._name = name
|
||||
self._unique_id = ads_var
|
||||
self._state_dict = {}
|
||||
self._state_dict[STATE_KEY_STATE] = None
|
||||
self._ads_hub = ads_hub
|
||||
self._ads_var = ads_var
|
||||
self._event = None
|
||||
|
||||
async def async_initialize_device(
|
||||
self, ads_var, plctype, state_key=STATE_KEY_STATE, factor=None):
|
||||
"""Register device notification."""
|
||||
def update(name, value):
|
||||
"""Handle device notifications."""
|
||||
_LOGGER.debug('Variable %s changed its value to %d', name, value)
|
||||
|
||||
if factor is None:
|
||||
self._state_dict[state_key] = value
|
||||
else:
|
||||
self._state_dict[state_key] = value / factor
|
||||
|
||||
asyncio.run_coroutine_threadsafe(async_event_set(), self.hass.loop)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
async def async_event_set():
|
||||
"""Set event in async context."""
|
||||
self._event.set()
|
||||
|
||||
self._event = asyncio.Event()
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self._ads_hub.add_device_notification,
|
||||
ads_var, plctype, update)
|
||||
try:
|
||||
with async_timeout.timeout(10):
|
||||
await self._event.wait()
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.debug('Variable %s: Timeout during first update',
|
||||
ads_var)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the default name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique identifier for this entity."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return False because entity pushes its state to HA."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return False if state has not been updated yet."""
|
||||
return self._state_dict[STATE_KEY_STATE] is not None
|
||||
|
||||
@@ -3,17 +3,16 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.ads import CONF_ADS_VAR, DATA_ADS
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import CONF_ADS_VAR, DATA_ADS, AdsEntity, STATE_KEY_STATE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'ADS binary sensor'
|
||||
DEPENDENCIES = ['ads']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ADS_VAR): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
@@ -33,51 +32,25 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
add_entities([ads_sensor])
|
||||
|
||||
|
||||
class AdsBinarySensor(BinarySensorDevice):
|
||||
class AdsBinarySensor(AdsEntity, BinarySensorDevice):
|
||||
"""Representation of ADS binary sensors."""
|
||||
|
||||
def __init__(self, ads_hub, name, ads_var, device_class):
|
||||
"""Initialize ADS binary sensor."""
|
||||
self._name = name
|
||||
self._unique_id = ads_var
|
||||
self._state = False
|
||||
super().__init__(ads_hub, name, ads_var)
|
||||
self._device_class = device_class or 'moving'
|
||||
self._ads_hub = ads_hub
|
||||
self.ads_var = ads_var
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register device notification."""
|
||||
def update(name, value):
|
||||
"""Handle device notifications."""
|
||||
_LOGGER.debug('Variable %s changed its value to %d', name, value)
|
||||
self._state = value
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
self.hass.async_add_job(
|
||||
self._ads_hub.add_device_notification,
|
||||
self.ads_var, self._ads_hub.PLCTYPE_BOOL, update)
|
||||
await self.async_initialize_device(self._ads_var,
|
||||
self._ads_hub.PLCTYPE_BOOL)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the default name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique identifier for this entity."""
|
||||
return self._unique_id
|
||||
def is_on(self):
|
||||
"""Return True if the entity is on."""
|
||||
return self._state_dict[STATE_KEY_STATE]
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return False because entity pushes its state to HA."""
|
||||
return False
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
"""Support for ADS light sources."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.light import Light, ATTR_BRIGHTNESS, \
|
||||
SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light)
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.ads import DATA_ADS, CONF_ADS_VAR, \
|
||||
CONF_ADS_VAR_BRIGHTNESS
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import CONF_ADS_VAR, CONF_ADS_VAR_BRIGHTNESS, DATA_ADS, \
|
||||
AdsEntity, STATE_KEY_BRIGHTNESS, STATE_KEY_STATE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['ads']
|
||||
DEFAULT_NAME = 'ADS Light'
|
||||
CONF_ADSVAR_BRIGHTNESS = 'adsvar_brightness'
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ADS_VAR): cv.string,
|
||||
vol.Optional(CONF_ADS_VAR_BRIGHTNESS): cv.string,
|
||||
@@ -28,91 +29,57 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
name = config.get(CONF_NAME)
|
||||
|
||||
add_entities([AdsLight(ads_hub, ads_var_enable, ads_var_brightness,
|
||||
name)], True)
|
||||
name)])
|
||||
|
||||
|
||||
class AdsLight(Light):
|
||||
class AdsLight(AdsEntity, Light):
|
||||
"""Representation of ADS light."""
|
||||
|
||||
def __init__(self, ads_hub, ads_var_enable, ads_var_brightness, name):
|
||||
"""Initialize AdsLight entity."""
|
||||
self._ads_hub = ads_hub
|
||||
self._on_state = False
|
||||
self._brightness = None
|
||||
self._name = name
|
||||
self._unique_id = ads_var_enable
|
||||
self.ads_var_enable = ads_var_enable
|
||||
self.ads_var_brightness = ads_var_brightness
|
||||
super().__init__(ads_hub, name, ads_var_enable)
|
||||
self._state_dict[STATE_KEY_BRIGHTNESS] = None
|
||||
self._ads_var_brightness = ads_var_brightness
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register device notification."""
|
||||
def update_on_state(name, value):
|
||||
"""Handle device notifications for state."""
|
||||
_LOGGER.debug('Variable %s changed its value to %d', name, value)
|
||||
self._on_state = value
|
||||
self.schedule_update_ha_state()
|
||||
await self.async_initialize_device(self._ads_var,
|
||||
self._ads_hub.PLCTYPE_BOOL)
|
||||
|
||||
def update_brightness(name, value):
|
||||
"""Handle device notification for brightness."""
|
||||
_LOGGER.debug('Variable %s changed its value to %d', name, value)
|
||||
self._brightness = value
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
self.hass.async_add_executor_job(
|
||||
self._ads_hub.add_device_notification,
|
||||
self.ads_var_enable, self._ads_hub.PLCTYPE_BOOL, update_on_state
|
||||
)
|
||||
if self.ads_var_brightness is not None:
|
||||
self.hass.async_add_executor_job(
|
||||
self._ads_hub.add_device_notification,
|
||||
self.ads_var_brightness, self._ads_hub.PLCTYPE_INT,
|
||||
update_brightness
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique identifier for this entity."""
|
||||
return self._unique_id
|
||||
if self._ads_var_brightness is not None:
|
||||
await self.async_initialize_device(self._ads_var_brightness,
|
||||
self._ads_hub.PLCTYPE_UINT,
|
||||
STATE_KEY_BRIGHTNESS)
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of the light (0..255)."""
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return if light is on."""
|
||||
return self._on_state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return False because entity pushes its state to HA."""
|
||||
return False
|
||||
return self._state_dict[STATE_KEY_BRIGHTNESS]
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
support = 0
|
||||
if self.ads_var_brightness is not None:
|
||||
if self._ads_var_brightness is not None:
|
||||
support = SUPPORT_BRIGHTNESS
|
||||
return support
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the entity is on."""
|
||||
return self._state_dict[STATE_KEY_STATE]
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the light on or set a specific dimmer value."""
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
self._ads_hub.write_by_name(self.ads_var_enable, True,
|
||||
self._ads_hub.write_by_name(self._ads_var, True,
|
||||
self._ads_hub.PLCTYPE_BOOL)
|
||||
|
||||
if self.ads_var_brightness is not None and brightness is not None:
|
||||
self._ads_hub.write_by_name(self.ads_var_brightness, brightness,
|
||||
if self._ads_var_brightness is not None and brightness is not None:
|
||||
self._ads_hub.write_by_name(self._ads_var_brightness, brightness,
|
||||
self._ads_hub.PLCTYPE_UINT)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the light off."""
|
||||
self._ads_hub.write_by_name(self.ads_var_enable, False,
|
||||
self._ads_hub.write_by_name(self._ads_var, False,
|
||||
self._ads_hub.PLCTYPE_BOOL)
|
||||
|
||||
10
homeassistant/components/ads/manifest.json
Normal file
10
homeassistant/components/ads/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "ads",
|
||||
"name": "Ads",
|
||||
"documentation": "https://www.home-assistant.io/components/ads",
|
||||
"requirements": [
|
||||
"pyads==3.0.7"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
}
|
||||
@@ -4,18 +4,16 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import ads
|
||||
from homeassistant.components.ads import (
|
||||
CONF_ADS_FACTOR, CONF_ADS_TYPE, CONF_ADS_VAR)
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from . import CONF_ADS_FACTOR, CONF_ADS_TYPE, CONF_ADS_VAR, \
|
||||
AdsEntity, STATE_KEY_STATE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "ADS sensor"
|
||||
DEPENDENCIES = ['ads']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ADS_VAR): cv.string,
|
||||
vol.Optional(CONF_ADS_FACTOR): cv.positive_int,
|
||||
@@ -43,60 +41,31 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
add_entities([entity])
|
||||
|
||||
|
||||
class AdsSensor(Entity):
|
||||
class AdsSensor(AdsEntity):
|
||||
"""Representation of an ADS sensor entity."""
|
||||
|
||||
def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement,
|
||||
factor):
|
||||
"""Initialize AdsSensor entity."""
|
||||
self._ads_hub = ads_hub
|
||||
self._name = name
|
||||
self._unique_id = ads_var
|
||||
self._value = None
|
||||
super().__init__(ads_hub, name, ads_var)
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self.ads_var = ads_var
|
||||
self.ads_type = ads_type
|
||||
self.factor = factor
|
||||
self._ads_type = ads_type
|
||||
self._factor = factor
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register device notification."""
|
||||
def update(name, value):
|
||||
"""Handle device notifications."""
|
||||
_LOGGER.debug("Variable %s changed its value to %d", name, value)
|
||||
|
||||
# If factor is set use it otherwise not
|
||||
if self.factor is None:
|
||||
self._value = value
|
||||
else:
|
||||
self._value = value / self.factor
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
self.hass.async_add_job(
|
||||
self._ads_hub.add_device_notification,
|
||||
self.ads_var, self._ads_hub.ADS_TYPEMAP[self.ads_type], update
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique identifier for this entity."""
|
||||
return self._unique_id
|
||||
await self.async_initialize_device(
|
||||
self._ads_var,
|
||||
self._ads_hub.ADS_TYPEMAP[self._ads_type],
|
||||
STATE_KEY_STATE,
|
||||
self._factor)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._value
|
||||
return self._state_dict[STATE_KEY_STATE]
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return False because entity pushes its state."""
|
||||
return False
|
||||
|
||||
@@ -3,16 +3,14 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.ads import CONF_ADS_VAR, DATA_ADS
|
||||
from homeassistant.components.switch import PLATFORM_SCHEMA
|
||||
from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
from . import CONF_ADS_VAR, DATA_ADS, AdsEntity, STATE_KEY_STATE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['ads']
|
||||
|
||||
DEFAULT_NAME = 'ADS Switch'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -28,58 +26,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
name = config.get(CONF_NAME)
|
||||
ads_var = config.get(CONF_ADS_VAR)
|
||||
|
||||
add_entities([AdsSwitch(ads_hub, name, ads_var)], True)
|
||||
add_entities([AdsSwitch(ads_hub, name, ads_var)])
|
||||
|
||||
|
||||
class AdsSwitch(ToggleEntity):
|
||||
class AdsSwitch(AdsEntity, SwitchDevice):
|
||||
"""Representation of an ADS switch device."""
|
||||
|
||||
def __init__(self, ads_hub, name, ads_var):
|
||||
"""Initialize the AdsSwitch entity."""
|
||||
self._ads_hub = ads_hub
|
||||
self._on_state = False
|
||||
self._name = name
|
||||
self._unique_id = ads_var
|
||||
self.ads_var = ads_var
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register device notification."""
|
||||
def update(name, value):
|
||||
"""Handle device notification."""
|
||||
_LOGGER.debug("Variable %s changed its value to %d", name, value)
|
||||
self._on_state = value
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
self.hass.async_add_job(
|
||||
self._ads_hub.add_device_notification,
|
||||
self.ads_var, self._ads_hub.PLCTYPE_BOOL, update)
|
||||
await self.async_initialize_device(self._ads_var,
|
||||
self._ads_hub.PLCTYPE_BOOL)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return if the switch is turned on."""
|
||||
return self._on_state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique identifier for this entity."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return False because entity pushes its state to HA."""
|
||||
return False
|
||||
"""Return True if the entity is on."""
|
||||
return self._state_dict[STATE_KEY_STATE]
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the switch on."""
|
||||
self._ads_hub.write_by_name(
|
||||
self.ads_var, True, self._ads_hub.PLCTYPE_BOOL)
|
||||
self._ads_var, True, self._ads_hub.PLCTYPE_BOOL)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the switch off."""
|
||||
self._ads_hub.write_by_name(
|
||||
self.ads_var, False, self._ads_hub.PLCTYPE_BOOL)
|
||||
self._ads_var, False, self._ads_hub.PLCTYPE_BOOL)
|
||||
|
||||
1
homeassistant/components/aftership/__init__.py
Normal file
1
homeassistant/components/aftership/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""The aftership component."""
|
||||
2
homeassistant/components/aftership/const.py
Normal file
2
homeassistant/components/aftership/const.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""Constants for the Aftership integration."""
|
||||
DOMAIN = 'aftership'
|
||||
10
homeassistant/components/aftership/manifest.json
Normal file
10
homeassistant/components/aftership/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "aftership",
|
||||
"name": "Aftership",
|
||||
"documentation": "https://www.home-assistant.io/components/aftership",
|
||||
"requirements": [
|
||||
"pyaftership==0.1.2"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
}
|
||||
204
homeassistant/components/aftership/sensor.py
Normal file
204
homeassistant/components/aftership/sensor.py
Normal file
@@ -0,0 +1,204 @@
|
||||
"""Support for non-delivered packages recorded in AfterShip."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTRIBUTION = 'Information provided by AfterShip'
|
||||
ATTR_TRACKINGS = 'trackings'
|
||||
|
||||
BASE = 'https://track.aftership.com/'
|
||||
|
||||
CONF_SLUG = 'slug'
|
||||
CONF_TITLE = 'title'
|
||||
CONF_TRACKING_NUMBER = 'tracking_number'
|
||||
|
||||
DEFAULT_NAME = 'aftership'
|
||||
UPDATE_TOPIC = DOMAIN + '_update'
|
||||
|
||||
ICON = 'mdi:package-variant-closed'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
||||
|
||||
SERVICE_ADD_TRACKING = 'add_tracking'
|
||||
SERVICE_REMOVE_TRACKING = 'remove_tracking'
|
||||
|
||||
ADD_TRACKING_SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TRACKING_NUMBER): cv.string,
|
||||
vol.Optional(CONF_TITLE): cv.string,
|
||||
vol.Optional(CONF_SLUG): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
REMOVE_TRACKING_SERVICE_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_SLUG): cv.string,
|
||||
vol.Required(CONF_TRACKING_NUMBER): cv.string}
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the AfterShip sensor platform."""
|
||||
from pyaftership.tracker import Tracking
|
||||
|
||||
apikey = config[CONF_API_KEY]
|
||||
name = config[CONF_NAME]
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
aftership = Tracking(hass.loop, session, apikey)
|
||||
|
||||
await aftership.get_trackings()
|
||||
|
||||
if not aftership.meta or aftership.meta['code'] != 200:
|
||||
_LOGGER.error("No tracking data found. Check API key is correct: %s",
|
||||
aftership.meta)
|
||||
return
|
||||
|
||||
instance = AfterShipSensor(aftership, name)
|
||||
|
||||
async_add_entities([instance], True)
|
||||
|
||||
async def handle_add_tracking(call):
|
||||
"""Call when a user adds a new Aftership tracking from HASS."""
|
||||
title = call.data.get(CONF_TITLE)
|
||||
slug = call.data.get(CONF_SLUG)
|
||||
tracking_number = call.data[CONF_TRACKING_NUMBER]
|
||||
|
||||
await aftership.add_package_tracking(tracking_number, title, slug)
|
||||
async_dispatcher_send(hass, UPDATE_TOPIC)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_ADD_TRACKING,
|
||||
handle_add_tracking,
|
||||
schema=ADD_TRACKING_SERVICE_SCHEMA,
|
||||
)
|
||||
|
||||
async def handle_remove_tracking(call):
|
||||
"""Call when a user removes an Aftership tracking from HASS."""
|
||||
slug = call.data[CONF_SLUG]
|
||||
tracking_number = call.data[CONF_TRACKING_NUMBER]
|
||||
|
||||
await aftership.remove_package_tracking(slug, tracking_number)
|
||||
async_dispatcher_send(hass, UPDATE_TOPIC)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_REMOVE_TRACKING,
|
||||
handle_remove_tracking,
|
||||
schema=REMOVE_TRACKING_SERVICE_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
class AfterShipSensor(Entity):
|
||||
"""Representation of a AfterShip sensor."""
|
||||
|
||||
def __init__(self, aftership, name):
|
||||
"""Initialize the sensor."""
|
||||
self._attributes = {}
|
||||
self._name = name
|
||||
self._state = None
|
||||
self.aftership = aftership
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return 'packages'
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return attributes for the sensor."""
|
||||
return self._attributes
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend."""
|
||||
return ICON
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
UPDATE_TOPIC, self.force_update)
|
||||
|
||||
async def force_update(self):
|
||||
"""Force update of data."""
|
||||
await self.async_update(no_throttle=True)
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
async def async_update(self, **kwargs):
|
||||
"""Get the latest data from the AfterShip API."""
|
||||
await self.aftership.get_trackings()
|
||||
|
||||
if not self.aftership.meta:
|
||||
_LOGGER.error("Unknown errors when querying")
|
||||
return
|
||||
if self.aftership.meta['code'] != 200:
|
||||
_LOGGER.error(
|
||||
"Errors when querying AfterShip. %s", str(self.aftership.meta))
|
||||
return
|
||||
|
||||
status_to_ignore = {'delivered'}
|
||||
status_counts = {}
|
||||
trackings = []
|
||||
not_delivered_count = 0
|
||||
|
||||
for track in self.aftership.trackings['trackings']:
|
||||
status = track['tag'].lower()
|
||||
name = (
|
||||
track['tracking_number']
|
||||
if track['title'] is None
|
||||
else track['title']
|
||||
)
|
||||
status_counts[status] = status_counts.get(status, 0) + 1
|
||||
trackings.append({
|
||||
'name': name,
|
||||
'tracking_number': track['tracking_number'],
|
||||
'slug': track['slug'],
|
||||
'link': '%s%s/%s' %
|
||||
(BASE, track['slug'], track['tracking_number']),
|
||||
'last_update': track['updated_at'],
|
||||
'expected_delivery': track['expected_delivery'],
|
||||
'status': track['tag'],
|
||||
'last_checkpoint': track['checkpoints'][-1]
|
||||
})
|
||||
|
||||
if status not in status_to_ignore:
|
||||
not_delivered_count += 1
|
||||
else:
|
||||
_LOGGER.debug("Ignoring %s as it has status: %s", name, status)
|
||||
|
||||
self._attributes = {
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
**status_counts,
|
||||
ATTR_TRACKINGS: trackings,
|
||||
}
|
||||
|
||||
self._state = not_delivered_count
|
||||
24
homeassistant/components/aftership/services.yaml
Normal file
24
homeassistant/components/aftership/services.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
# Describes the format for available aftership services
|
||||
|
||||
add_tracking:
|
||||
description: Add new tracking to Aftership.
|
||||
fields:
|
||||
tracking_number:
|
||||
description: Tracking number for the new tracking
|
||||
example: '123456789'
|
||||
title:
|
||||
description: A custom title for the new tracking
|
||||
example: 'Laptop'
|
||||
slug:
|
||||
description: Slug (carrier) of the new tracking
|
||||
example: 'USPS'
|
||||
|
||||
remove_tracking:
|
||||
description: Remove a tracking from Aftership.
|
||||
fields:
|
||||
tracking_number:
|
||||
description: Tracking number of the tracking to remove
|
||||
example: '123456789'
|
||||
slug:
|
||||
description: Slug (carrier) of the tracking to remove
|
||||
example: 'USPS'
|
||||
@@ -1,9 +1,4 @@
|
||||
"""
|
||||
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/
|
||||
"""
|
||||
"""Component for handling Air Quality data for your location."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
"""
|
||||
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'
|
||||
8
homeassistant/components/air_quality/manifest.json
Normal file
8
homeassistant/components/air_quality/manifest.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"domain": "air_quality",
|
||||
"name": "Air quality",
|
||||
"documentation": "https://www.home-assistant.io/components/air_quality",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
"""
|
||||
Sensor for checking the air quality around Norway.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/air_quality.nilu/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.air_quality import (
|
||||
PLATFORM_SCHEMA, AirQualityEntity)
|
||||
from homeassistant.const import (
|
||||
CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_SHOW_ON_MAP)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['niluclient==0.1.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_AREA = 'area'
|
||||
ATTR_POLLUTION_INDEX = 'nilu_pollution_index'
|
||||
ATTRIBUTION = "Data provided by luftkvalitet.info and nilu.no"
|
||||
|
||||
CONF_AREA = 'area'
|
||||
CONF_STATION = 'stations'
|
||||
|
||||
DEFAULT_NAME = 'NILU'
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=30)
|
||||
|
||||
CONF_ALLOWED_AREAS = [
|
||||
'Bergen',
|
||||
'Birkenes',
|
||||
'Bodø',
|
||||
'Brumunddal',
|
||||
'Bærum',
|
||||
'Drammen',
|
||||
'Elverum',
|
||||
'Fredrikstad',
|
||||
'Gjøvik',
|
||||
'Grenland',
|
||||
'Halden',
|
||||
'Hamar',
|
||||
'Harstad',
|
||||
'Hurdal',
|
||||
'Karasjok',
|
||||
'Kristiansand',
|
||||
'Kårvatn',
|
||||
'Lillehammer',
|
||||
'Lillesand',
|
||||
'Lillestrøm',
|
||||
'Lørenskog',
|
||||
'Mo i Rana',
|
||||
'Moss',
|
||||
'Narvik',
|
||||
'Oslo',
|
||||
'Prestebakke',
|
||||
'Sandve',
|
||||
'Sarpsborg',
|
||||
'Stavanger',
|
||||
'Sør-Varanger',
|
||||
'Tromsø',
|
||||
'Trondheim',
|
||||
'Tustervatn',
|
||||
'Zeppelinfjellet',
|
||||
'Ålesund',
|
||||
]
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Inclusive(CONF_LATITUDE, 'coordinates',
|
||||
'Latitude and longitude must exist together'): cv.latitude,
|
||||
vol.Inclusive(CONF_LONGITUDE, 'coordinates',
|
||||
'Latitude and longitude must exist together'): cv.longitude,
|
||||
vol.Exclusive(CONF_AREA, 'station_collection',
|
||||
'Can only configure one specific station or '
|
||||
'stations in a specific area pr sensor. '
|
||||
'Please only configure station or area.'
|
||||
): vol.All(cv.string, vol.In(CONF_ALLOWED_AREAS)),
|
||||
vol.Exclusive(CONF_STATION, 'station_collection',
|
||||
'Can only configure one specific station or '
|
||||
'stations in a specific area pr sensor. '
|
||||
'Please only configure station or area.'
|
||||
): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the NILU air quality sensor."""
|
||||
import niluclient as nilu
|
||||
name = config.get(CONF_NAME)
|
||||
area = config.get(CONF_AREA)
|
||||
stations = config.get(CONF_STATION)
|
||||
show_on_map = config.get(CONF_SHOW_ON_MAP)
|
||||
|
||||
sensors = []
|
||||
|
||||
if area:
|
||||
stations = nilu.lookup_stations_in_area(area)
|
||||
elif not area and not stations:
|
||||
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||||
location_client = nilu.create_location_client(latitude, longitude)
|
||||
stations = location_client.station_names
|
||||
|
||||
for station in stations:
|
||||
client = NiluData(nilu.create_station_client(station))
|
||||
client.update()
|
||||
if client.data.sensors:
|
||||
sensors.append(NiluSensor(client, name, show_on_map))
|
||||
else:
|
||||
_LOGGER.warning("%s didn't give any sensors results", station)
|
||||
|
||||
add_entities(sensors, True)
|
||||
|
||||
|
||||
class NiluData:
|
||||
"""Class for handling the data retrieval."""
|
||||
|
||||
def __init__(self, api):
|
||||
"""Initialize the data object."""
|
||||
self.api = api
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
"""Get data cached in client."""
|
||||
return self.api.data
|
||||
|
||||
@Throttle(SCAN_INTERVAL)
|
||||
def update(self):
|
||||
"""Get the latest data from nilu API."""
|
||||
self.api.update()
|
||||
|
||||
|
||||
class NiluSensor(AirQualityEntity):
|
||||
"""Single nilu station air sensor."""
|
||||
|
||||
def __init__(self, api_data: NiluData, name: str, show_on_map: bool):
|
||||
"""Initialize the sensor."""
|
||||
self._api = api_data
|
||||
self._name = "{} {}".format(name, api_data.data.name)
|
||||
self._max_aqi = None
|
||||
self._attrs = {}
|
||||
|
||||
if show_on_map:
|
||||
self._attrs[CONF_LATITUDE] = api_data.data.latitude
|
||||
self._attrs[CONF_LONGITUDE] = api_data.data.longitude
|
||||
|
||||
@property
|
||||
def attribution(self) -> str:
|
||||
"""Return the attribution."""
|
||||
return ATTRIBUTION
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> dict:
|
||||
"""Return other details about the sensor state."""
|
||||
return self._attrs
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def air_quality_index(self) -> str:
|
||||
"""Return the Air Quality Index (AQI)."""
|
||||
return self._max_aqi
|
||||
|
||||
@property
|
||||
def carbon_monoxide(self) -> str:
|
||||
"""Return the CO (carbon monoxide) level."""
|
||||
from niluclient import CO
|
||||
return self.get_component_state(CO)
|
||||
|
||||
@property
|
||||
def carbon_dioxide(self) -> str:
|
||||
"""Return the CO2 (carbon dioxide) level."""
|
||||
from niluclient import CO2
|
||||
return self.get_component_state(CO2)
|
||||
|
||||
@property
|
||||
def nitrogen_oxide(self) -> str:
|
||||
"""Return the N2O (nitrogen oxide) level."""
|
||||
from niluclient import NOX
|
||||
return self.get_component_state(NOX)
|
||||
|
||||
@property
|
||||
def nitrogen_monoxide(self) -> str:
|
||||
"""Return the NO (nitrogen monoxide) level."""
|
||||
from niluclient import NO
|
||||
return self.get_component_state(NO)
|
||||
|
||||
@property
|
||||
def nitrogen_dioxide(self) -> str:
|
||||
"""Return the NO2 (nitrogen dioxide) level."""
|
||||
from niluclient import NO2
|
||||
return self.get_component_state(NO2)
|
||||
|
||||
@property
|
||||
def ozone(self) -> str:
|
||||
"""Return the O3 (ozone) level."""
|
||||
from niluclient import OZONE
|
||||
return self.get_component_state(OZONE)
|
||||
|
||||
@property
|
||||
def particulate_matter_2_5(self) -> str:
|
||||
"""Return the particulate matter 2.5 level."""
|
||||
from niluclient import PM25
|
||||
return self.get_component_state(PM25)
|
||||
|
||||
@property
|
||||
def particulate_matter_10(self) -> str:
|
||||
"""Return the particulate matter 10 level."""
|
||||
from niluclient import PM10
|
||||
return self.get_component_state(PM10)
|
||||
|
||||
@property
|
||||
def particulate_matter_0_1(self) -> str:
|
||||
"""Return the particulate matter 0.1 level."""
|
||||
from niluclient import PM1
|
||||
return self.get_component_state(PM1)
|
||||
|
||||
@property
|
||||
def sulphur_dioxide(self) -> str:
|
||||
"""Return the SO2 (sulphur dioxide) level."""
|
||||
from niluclient import SO2
|
||||
return self.get_component_state(SO2)
|
||||
|
||||
def get_component_state(self, component_name: str) -> str:
|
||||
"""Return formatted value of specified component."""
|
||||
if component_name in self._api.data.sensors:
|
||||
sensor = self._api.data.sensors[component_name]
|
||||
return sensor.value
|
||||
return None
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update the sensor."""
|
||||
import niluclient as nilu
|
||||
self._api.update()
|
||||
|
||||
sensors = self._api.data.sensors.values()
|
||||
if sensors:
|
||||
max_index = max([s.pollution_index for s in sensors])
|
||||
self._max_aqi = max_index
|
||||
self._attrs[ATTR_POLLUTION_INDEX] = \
|
||||
nilu.POLLUTION_INDEX[self._max_aqi]
|
||||
|
||||
self._attrs[ATTR_AREA] = self._api.data.area
|
||||
@@ -1,140 +0,0 @@
|
||||
"""
|
||||
Sensor for checking the air quality forecast around Norway.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/air_quality.norway_air/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.air_quality import (
|
||||
PLATFORM_SCHEMA, AirQualityEntity)
|
||||
from homeassistant.const import (CONF_LATITUDE, CONF_LONGITUDE,
|
||||
CONF_NAME)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
|
||||
REQUIREMENTS = ['pyMetno==0.4.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTRIBUTION = "Air quality from " \
|
||||
"https://luftkvalitet.miljostatus.no/, " \
|
||||
"delivered by the Norwegian Meteorological Institute."
|
||||
# https://api.met.no/license_data.html
|
||||
|
||||
CONF_FORECAST = 'forecast'
|
||||
|
||||
DEFAULT_FORECAST = 0
|
||||
DEFAULT_NAME = 'Air quality Norway'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_FORECAST, default=DEFAULT_FORECAST): vol.Coerce(int),
|
||||
vol.Optional(CONF_LATITUDE): cv.latitude,
|
||||
vol.Optional(CONF_LONGITUDE): cv.longitude,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the air_quality norway sensor."""
|
||||
forecast = config.get(CONF_FORECAST)
|
||||
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||||
name = config.get(CONF_NAME)
|
||||
|
||||
if None in (latitude, longitude):
|
||||
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
||||
return
|
||||
|
||||
coordinates = {
|
||||
'lat': str(latitude),
|
||||
'lon': str(longitude),
|
||||
}
|
||||
|
||||
async_add_entities([AirSensor(name, coordinates,
|
||||
forecast, async_get_clientsession(hass),
|
||||
)],
|
||||
True)
|
||||
|
||||
|
||||
def round_state(func):
|
||||
"""Round state."""
|
||||
def _decorator(self):
|
||||
res = func(self)
|
||||
if isinstance(res, float):
|
||||
return round(res, 2)
|
||||
return res
|
||||
return _decorator
|
||||
|
||||
|
||||
class AirSensor(AirQualityEntity):
|
||||
"""Representation of an Yr.no sensor."""
|
||||
|
||||
def __init__(self, name, coordinates, forecast, session):
|
||||
"""Initialize the sensor."""
|
||||
import metno
|
||||
self._name = name
|
||||
self._api = metno.AirQualityData(coordinates, forecast, session)
|
||||
|
||||
@property
|
||||
def attribution(self) -> str:
|
||||
"""Return the attribution."""
|
||||
return ATTRIBUTION
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> dict:
|
||||
"""Return other details about the sensor state."""
|
||||
return {'level': self._api.data.get('level'),
|
||||
'location': self._api.data.get('location'),
|
||||
}
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
@round_state
|
||||
def air_quality_index(self):
|
||||
"""Return the Air Quality Index (AQI)."""
|
||||
return self._api.data.get('aqi')
|
||||
|
||||
@property
|
||||
@round_state
|
||||
def nitrogen_dioxide(self):
|
||||
"""Return the NO2 (nitrogen dioxide) level."""
|
||||
return self._api.data.get('no2_concentration')
|
||||
|
||||
@property
|
||||
@round_state
|
||||
def ozone(self):
|
||||
"""Return the O3 (ozone) level."""
|
||||
return self._api.data.get('o3_concentration')
|
||||
|
||||
@property
|
||||
@round_state
|
||||
def particulate_matter_2_5(self):
|
||||
"""Return the particulate matter 2.5 level."""
|
||||
return self._api.data.get('pm25_concentration')
|
||||
|
||||
@property
|
||||
@round_state
|
||||
def particulate_matter_10(self):
|
||||
"""Return the particulate matter 10 level."""
|
||||
return self._api.data.get('pm10_concentration')
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._api.units.get('pm25_concentration')
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the sensor."""
|
||||
await self._api.update()
|
||||
@@ -1,100 +0,0 @@
|
||||
"""Support for openSenseMap Air Quality data."""
|
||||
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.5']
|
||||
|
||||
_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)
|
||||
1
homeassistant/components/airvisual/__init__.py
Normal file
1
homeassistant/components/airvisual/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""The airvisual component."""
|
||||
12
homeassistant/components/airvisual/manifest.json
Normal file
12
homeassistant/components/airvisual/manifest.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "airvisual",
|
||||
"name": "Airvisual",
|
||||
"documentation": "https://www.home-assistant.io/components/airvisual",
|
||||
"requirements": [
|
||||
"pyairvisual==3.0.1"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@bachya"
|
||||
]
|
||||
}
|
||||
294
homeassistant/components/airvisual/sensor.py
Normal file
294
homeassistant/components/airvisual/sensor.py
Normal file
@@ -0,0 +1,294 @@
|
||||
"""Support for AirVisual air quality sensors."""
|
||||
from logging import getLogger
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_API_KEY,
|
||||
CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS,
|
||||
CONF_SCAN_INTERVAL, CONF_STATE, CONF_SHOW_ON_MAP)
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
ATTR_CITY = 'city'
|
||||
ATTR_COUNTRY = 'country'
|
||||
ATTR_POLLUTANT_SYMBOL = 'pollutant_symbol'
|
||||
ATTR_POLLUTANT_UNIT = 'pollutant_unit'
|
||||
ATTR_REGION = 'region'
|
||||
|
||||
CONF_CITY = 'city'
|
||||
CONF_COUNTRY = 'country'
|
||||
|
||||
DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
MASS_PARTS_PER_MILLION = 'ppm'
|
||||
MASS_PARTS_PER_BILLION = 'ppb'
|
||||
VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3'
|
||||
|
||||
SENSOR_TYPE_LEVEL = 'air_pollution_level'
|
||||
SENSOR_TYPE_AQI = 'air_quality_index'
|
||||
SENSOR_TYPE_POLLUTANT = 'main_pollutant'
|
||||
SENSORS = [
|
||||
(SENSOR_TYPE_LEVEL, 'Air Pollution Level', 'mdi:gauge', None),
|
||||
(SENSOR_TYPE_AQI, 'Air Quality Index', 'mdi:chart-line', 'AQI'),
|
||||
(SENSOR_TYPE_POLLUTANT, 'Main Pollutant', 'mdi:chemical-weapon', None),
|
||||
]
|
||||
|
||||
POLLUTANT_LEVEL_MAPPING = [{
|
||||
'label': 'Good',
|
||||
'icon': 'mdi:emoticon-excited',
|
||||
'minimum': 0,
|
||||
'maximum': 50
|
||||
}, {
|
||||
'label': 'Moderate',
|
||||
'icon': 'mdi:emoticon-happy',
|
||||
'minimum': 51,
|
||||
'maximum': 100
|
||||
}, {
|
||||
'label': 'Unhealthy for sensitive groups',
|
||||
'icon': 'mdi:emoticon-neutral',
|
||||
'minimum': 101,
|
||||
'maximum': 150
|
||||
}, {
|
||||
'label': 'Unhealthy',
|
||||
'icon': 'mdi:emoticon-sad',
|
||||
'minimum': 151,
|
||||
'maximum': 200
|
||||
}, {
|
||||
'label': 'Very Unhealthy',
|
||||
'icon': 'mdi:emoticon-dead',
|
||||
'minimum': 201,
|
||||
'maximum': 300
|
||||
}, {
|
||||
'label': 'Hazardous',
|
||||
'icon': 'mdi:biohazard',
|
||||
'minimum': 301,
|
||||
'maximum': 10000
|
||||
}]
|
||||
|
||||
POLLUTANT_MAPPING = {
|
||||
'co': {
|
||||
'label': 'Carbon Monoxide',
|
||||
'unit': MASS_PARTS_PER_MILLION
|
||||
},
|
||||
'n2': {
|
||||
'label': 'Nitrogen Dioxide',
|
||||
'unit': MASS_PARTS_PER_BILLION
|
||||
},
|
||||
'o3': {
|
||||
'label': 'Ozone',
|
||||
'unit': MASS_PARTS_PER_BILLION
|
||||
},
|
||||
'p1': {
|
||||
'label': 'PM10',
|
||||
'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER
|
||||
},
|
||||
'p2': {
|
||||
'label': 'PM2.5',
|
||||
'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER
|
||||
},
|
||||
's2': {
|
||||
'label': 'Sulfur Dioxide',
|
||||
'unit': MASS_PARTS_PER_BILLION
|
||||
},
|
||||
}
|
||||
|
||||
SENSOR_LOCALES = {'cn': 'Chinese', 'us': 'U.S.'}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_LOCALES)):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_LOCALES)]),
|
||||
vol.Inclusive(CONF_CITY, 'city'): cv.string,
|
||||
vol.Inclusive(CONF_COUNTRY, 'city'): cv.string,
|
||||
vol.Inclusive(CONF_LATITUDE, 'coords'): cv.latitude,
|
||||
vol.Inclusive(CONF_LONGITUDE, 'coords'): cv.longitude,
|
||||
vol.Optional(CONF_SHOW_ON_MAP, default=True): cv.boolean,
|
||||
vol.Inclusive(CONF_STATE, 'city'): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
|
||||
cv.time_period
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Configure the platform and add the sensors."""
|
||||
from pyairvisual import Client
|
||||
|
||||
city = config.get(CONF_CITY)
|
||||
state = config.get(CONF_STATE)
|
||||
country = config.get(CONF_COUNTRY)
|
||||
|
||||
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||||
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
|
||||
if city and state and country:
|
||||
_LOGGER.debug(
|
||||
"Using city, state, and country: %s, %s, %s", city, state, country)
|
||||
location_id = ','.join((city, state, country))
|
||||
data = AirVisualData(
|
||||
Client(websession, api_key=config[CONF_API_KEY]),
|
||||
city=city,
|
||||
state=state,
|
||||
country=country,
|
||||
show_on_map=config[CONF_SHOW_ON_MAP],
|
||||
scan_interval=config[CONF_SCAN_INTERVAL])
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Using latitude and longitude: %s, %s", latitude, longitude)
|
||||
location_id = ','.join((str(latitude), str(longitude)))
|
||||
data = AirVisualData(
|
||||
Client(websession, api_key=config[CONF_API_KEY]),
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
show_on_map=config[CONF_SHOW_ON_MAP],
|
||||
scan_interval=config[CONF_SCAN_INTERVAL])
|
||||
|
||||
await data.async_update()
|
||||
|
||||
sensors = []
|
||||
for locale in config[CONF_MONITORED_CONDITIONS]:
|
||||
for kind, name, icon, unit in SENSORS:
|
||||
sensors.append(
|
||||
AirVisualSensor(
|
||||
data, kind, name, icon, unit, locale, location_id))
|
||||
|
||||
async_add_entities(sensors, True)
|
||||
|
||||
|
||||
class AirVisualSensor(Entity):
|
||||
"""Define an AirVisual sensor."""
|
||||
|
||||
def __init__(self, airvisual, kind, name, icon, unit, locale, location_id):
|
||||
"""Initialize."""
|
||||
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
|
||||
self._icon = icon
|
||||
self._locale = locale
|
||||
self._location_id = location_id
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._type = kind
|
||||
self._unit = unit
|
||||
self.airvisual = airvisual
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
if self.airvisual.show_on_map:
|
||||
self._attrs[ATTR_LATITUDE] = self.airvisual.latitude
|
||||
self._attrs[ATTR_LONGITUDE] = self.airvisual.longitude
|
||||
else:
|
||||
self._attrs['lati'] = self.airvisual.latitude
|
||||
self._attrs['long'] = self.airvisual.longitude
|
||||
|
||||
return self._attrs
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return bool(self.airvisual.pollution_info)
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name."""
|
||||
return '{0} {1}'.format(SENSOR_LOCALES[self._locale], self._name)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique, HASS-friendly identifier for this entity."""
|
||||
return '{0}_{1}_{2}'.format(
|
||||
self._location_id, self._locale, self._type)
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
return self._unit
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the sensor."""
|
||||
await self.airvisual.async_update()
|
||||
data = self.airvisual.pollution_info
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
if self._type == SENSOR_TYPE_LEVEL:
|
||||
aqi = data['aqi{0}'.format(self._locale)]
|
||||
[level] = [
|
||||
i for i in POLLUTANT_LEVEL_MAPPING
|
||||
if i['minimum'] <= aqi <= i['maximum']
|
||||
]
|
||||
self._state = level['label']
|
||||
self._icon = level['icon']
|
||||
elif self._type == SENSOR_TYPE_AQI:
|
||||
self._state = data['aqi{0}'.format(self._locale)]
|
||||
elif self._type == SENSOR_TYPE_POLLUTANT:
|
||||
symbol = data['main{0}'.format(self._locale)]
|
||||
self._state = POLLUTANT_MAPPING[symbol]['label']
|
||||
self._attrs.update({
|
||||
ATTR_POLLUTANT_SYMBOL: symbol,
|
||||
ATTR_POLLUTANT_UNIT: POLLUTANT_MAPPING[symbol]['unit']
|
||||
})
|
||||
|
||||
|
||||
class AirVisualData:
|
||||
"""Define an object to hold sensor data."""
|
||||
|
||||
def __init__(self, client, **kwargs):
|
||||
"""Initialize."""
|
||||
self._client = client
|
||||
self.city = kwargs.get(CONF_CITY)
|
||||
self.country = kwargs.get(CONF_COUNTRY)
|
||||
self.latitude = kwargs.get(CONF_LATITUDE)
|
||||
self.longitude = kwargs.get(CONF_LONGITUDE)
|
||||
self.pollution_info = {}
|
||||
self.show_on_map = kwargs.get(CONF_SHOW_ON_MAP)
|
||||
self.state = kwargs.get(CONF_STATE)
|
||||
|
||||
self.async_update = Throttle(
|
||||
kwargs[CONF_SCAN_INTERVAL])(self._async_update)
|
||||
|
||||
async def _async_update(self):
|
||||
"""Update AirVisual data."""
|
||||
from pyairvisual.errors import AirVisualError
|
||||
|
||||
try:
|
||||
if self.city and self.state and self.country:
|
||||
resp = await self._client.api.city(
|
||||
self.city, self.state, self.country)
|
||||
self.longitude, self.latitude = resp['location']['coordinates']
|
||||
else:
|
||||
resp = await self._client.api.nearest_city(
|
||||
self.latitude, self.longitude)
|
||||
|
||||
_LOGGER.debug("New data retrieved: %s", resp)
|
||||
|
||||
self.pollution_info = resp['current']['pollution']
|
||||
except (KeyError, AirVisualError) as err:
|
||||
if self.city and self.state and self.country:
|
||||
location = (self.city, self.state, self.country)
|
||||
else:
|
||||
location = (self.latitude, self.longitude)
|
||||
|
||||
_LOGGER.error(
|
||||
"Can't retrieve data for location: %s (%s)", location,
|
||||
err)
|
||||
self.pollution_info = {}
|
||||
1
homeassistant/components/aladdin_connect/__init__.py
Normal file
1
homeassistant/components/aladdin_connect/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""The aladdin_connect component."""
|
||||
113
homeassistant/components/aladdin_connect/cover.py
Normal file
113
homeassistant/components/aladdin_connect/cover.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""Platform for the Aladdin Connect cover component."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.cover import (CoverDevice, PLATFORM_SCHEMA,
|
||||
SUPPORT_OPEN, SUPPORT_CLOSE)
|
||||
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED,
|
||||
STATE_OPENING, STATE_CLOSING, STATE_OPEN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NOTIFICATION_ID = 'aladdin_notification'
|
||||
NOTIFICATION_TITLE = 'Aladdin Connect Cover Setup'
|
||||
|
||||
STATES_MAP = {
|
||||
'open': STATE_OPEN,
|
||||
'opening': STATE_OPENING,
|
||||
'closed': STATE_CLOSED,
|
||||
'closing': STATE_CLOSING
|
||||
}
|
||||
|
||||
SUPPORTED_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Aladdin Connect platform."""
|
||||
from aladdin_connect import AladdinConnectClient
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
acc = AladdinConnectClient(username, password)
|
||||
|
||||
try:
|
||||
if not acc.login():
|
||||
raise ValueError("Username or Password is incorrect")
|
||||
add_entities(AladdinDevice(acc, door) for door in acc.get_doors())
|
||||
except (TypeError, KeyError, NameError, ValueError) as ex:
|
||||
_LOGGER.error("%s", ex)
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
|
||||
|
||||
class AladdinDevice(CoverDevice):
|
||||
"""Representation of Aladdin Connect cover."""
|
||||
|
||||
def __init__(self, acc, device):
|
||||
"""Initialize the cover."""
|
||||
self._acc = acc
|
||||
self._device_id = device['device_id']
|
||||
self._number = device['door_number']
|
||||
self._name = device['name']
|
||||
self._status = STATES_MAP.get(device['status'])
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Define this cover as a garage door."""
|
||||
return 'garage'
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORTED_FEATURES
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return '{}-{}'.format(self._device_id, self._number)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the garage door."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_opening(self):
|
||||
"""Return if the cover is opening or not."""
|
||||
return self._status == STATE_OPENING
|
||||
|
||||
@property
|
||||
def is_closing(self):
|
||||
"""Return if the cover is closing or not."""
|
||||
return self._status == STATE_CLOSING
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return None if status is unknown, True if closed, else False."""
|
||||
if self._status is None:
|
||||
return None
|
||||
return self._status == STATE_CLOSED
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Issue close command to cover."""
|
||||
self._acc.close_door(self._device_id, self._number)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Issue open command to cover."""
|
||||
self._acc.open_door(self._device_id, self._number)
|
||||
|
||||
def update(self):
|
||||
"""Update status of cover."""
|
||||
acc_status = self._acc.get_door_status(self._device_id, self._number)
|
||||
self._status = STATES_MAP.get(acc_status)
|
||||
10
homeassistant/components/aladdin_connect/manifest.json
Normal file
10
homeassistant/components/aladdin_connect/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "aladdin_connect",
|
||||
"name": "Aladdin connect",
|
||||
"documentation": "https://www.home-assistant.io/components/aladdin_connect",
|
||||
"requirements": [
|
||||
"aladdin_connect==0.3"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
"""
|
||||
Component to interface with an alarm control panel.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel/
|
||||
"""
|
||||
"""Component to interface with an alarm control panel."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
"""
|
||||
Interfaces with Alarm.com alarm control panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.alarmdotcom/
|
||||
"""
|
||||
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_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyalarmdotcom==0.3.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Alarm.com'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
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,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up a Alarm.com control panel."""
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
alarmdotcom = AlarmDotCom(hass, name, code, username, password)
|
||||
await alarmdotcom.async_login()
|
||||
async_add_entities([alarmdotcom])
|
||||
|
||||
|
||||
class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
"""Representation of an Alarm.com status."""
|
||||
|
||||
def __init__(self, hass, name, code, username, password):
|
||||
"""Initialize the Alarm.com status."""
|
||||
from pyalarmdotcom import Alarmdotcom
|
||||
_LOGGER.debug('Setting up Alarm.com...')
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._websession = async_get_clientsession(self._hass)
|
||||
self._state = None
|
||||
self._alarm = Alarmdotcom(
|
||||
username, password, self._websession, hass.loop)
|
||||
|
||||
async def async_login(self):
|
||||
"""Login to Alarm.com."""
|
||||
await self._alarm.async_login()
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch the latest state."""
|
||||
await self._alarm.async_update()
|
||||
return self._alarm.state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the alarm."""
|
||||
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."""
|
||||
if self._alarm.state.lower() == 'disarmed':
|
||||
return STATE_ALARM_DISARMED
|
||||
if self._alarm.state.lower() == 'armed stay':
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
if self._alarm.state.lower() == 'armed away':
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
return None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'sensor_status': self._alarm.sensor_status
|
||||
}
|
||||
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if self._validate_code(code):
|
||||
await self._alarm.async_alarm_disarm()
|
||||
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm hom command."""
|
||||
if self._validate_code(code):
|
||||
await self._alarm.async_alarm_arm_home()
|
||||
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if self._validate_code(code):
|
||||
await self._alarm.async_alarm_arm_away()
|
||||
|
||||
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
|
||||
@@ -1,91 +0,0 @@
|
||||
"""
|
||||
Support for Canary alarm.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.canary/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.components.canary import DATA_CANARY
|
||||
from homeassistant.const import STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, \
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_HOME
|
||||
|
||||
DEPENDENCIES = ['canary']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Canary alarms."""
|
||||
data = hass.data[DATA_CANARY]
|
||||
devices = []
|
||||
|
||||
for location in data.locations:
|
||||
devices.append(CanaryAlarm(data, location.location_id))
|
||||
|
||||
add_entities(devices, True)
|
||||
|
||||
|
||||
class CanaryAlarm(AlarmControlPanel):
|
||||
"""Representation of a Canary alarm control panel."""
|
||||
|
||||
def __init__(self, data, location_id):
|
||||
"""Initialize a Canary security camera."""
|
||||
self._data = data
|
||||
self._location_id = location_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the alarm."""
|
||||
location = self._data.get_location(self._location_id)
|
||||
return location.name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, \
|
||||
LOCATION_MODE_NIGHT
|
||||
|
||||
location = self._data.get_location(self._location_id)
|
||||
|
||||
if location.is_private:
|
||||
return STATE_ALARM_DISARMED
|
||||
|
||||
mode = location.mode
|
||||
if mode.name == LOCATION_MODE_AWAY:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
if mode.name == LOCATION_MODE_HOME:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
if mode.name == LOCATION_MODE_NIGHT:
|
||||
return STATE_ALARM_ARMED_NIGHT
|
||||
return None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
location = self._data.get_location(self._location_id)
|
||||
return {
|
||||
'private': location.is_private
|
||||
}
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
location = self._data.get_location(self._location_id)
|
||||
self._data.set_location_mode(self._location_id, location.mode.name,
|
||||
True)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
from canary.api import LOCATION_MODE_HOME
|
||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
from canary.api import LOCATION_MODE_AWAY
|
||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
|
||||
|
||||
def alarm_arm_night(self, code=None):
|
||||
"""Send arm night command."""
|
||||
from canary.api import LOCATION_MODE_NIGHT
|
||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT)
|
||||
@@ -1,109 +0,0 @@
|
||||
"""
|
||||
Support for Concord232 alarm control panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.concord232/
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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)
|
||||
|
||||
REQUIREMENTS = ['concord232==0.15']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_NAME = 'CONCORD232'
|
||||
DEFAULT_PORT = 5007
|
||||
|
||||
SCAN_INTERVAL = datetime.timedelta(seconds=10)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Concord232 alarm control panel platform."""
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
|
||||
url = 'http://{}:{}'.format(host, port)
|
||||
|
||||
try:
|
||||
add_entities([Concord232Alarm(url, name)], True)
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||
|
||||
|
||||
class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
"""Representation of the Concord232-based alarm panel."""
|
||||
|
||||
def __init__(self, url, name):
|
||||
"""Initialize the Concord232 alarm panel."""
|
||||
from concord232 import client as concord232_client
|
||||
|
||||
self._state = None
|
||||
self._name = name
|
||||
self._url = url
|
||||
self._alarm = concord232_client.Client(self._url)
|
||||
self._alarm.partitions = self._alarm.list_partitions()
|
||||
self._alarm.last_partition_update = datetime.datetime.now()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the characters if code is defined."""
|
||||
return alarm.FORMAT_NUMBER
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update values from API."""
|
||||
try:
|
||||
part = self._alarm.list_partitions()[0]
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to %(host)s: %(reason)s",
|
||||
dict(host=self._url, reason=ex))
|
||||
return
|
||||
except IndexError:
|
||||
_LOGGER.error("Concord232 reports no partitions")
|
||||
return
|
||||
|
||||
if part['arming_level'] == 'Off':
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif 'Home' in part['arming_level']:
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._alarm.disarm(code)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._alarm.arm('stay')
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._alarm.arm('away')
|
||||
@@ -1,49 +0,0 @@
|
||||
"""
|
||||
Demo platform that has two fake alarm control panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import datetime
|
||||
from homeassistant.components.alarm_control_panel import manual
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, CONF_DELAY_TIME,
|
||||
CONF_PENDING_TIME, CONF_TRIGGER_TIME)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the Demo alarm control panel platform."""
|
||||
async_add_entities([
|
||||
manual.ManualAlarm(hass, 'Alarm', '1234', None, False, {
|
||||
STATE_ALARM_ARMED_AWAY: {
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_ARMED_HOME: {
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_ARMED_NIGHT: {
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_DISARMED: {
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS: {
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_TRIGGERED: {
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
},
|
||||
}),
|
||||
])
|
||||
@@ -1,133 +0,0 @@
|
||||
"""
|
||||
Interfaces with iAlarm control panels.
|
||||
|
||||
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_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']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'iAlarm'
|
||||
|
||||
|
||||
def no_application_protocol(value):
|
||||
"""Validate that value is without the application protocol."""
|
||||
protocol_separator = "://"
|
||||
if not value or protocol_separator in value:
|
||||
raise vol.Invalid(
|
||||
'Invalid host, {} is not allowed'.format(protocol_separator))
|
||||
|
||||
return value
|
||||
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
|
||||
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, code, username, password, url)
|
||||
add_entities([ialarm], True)
|
||||
|
||||
|
||||
class IAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Representation of an iAlarm status."""
|
||||
|
||||
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
|
||||
self._state = None
|
||||
self._client = IAlarm(username, password, url)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""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."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Return the state of the device."""
|
||||
status = self._client.get_status()
|
||||
_LOGGER.debug('iAlarm status: %s', status)
|
||||
if status:
|
||||
status = int(status)
|
||||
|
||||
if status == self._client.DISARMED:
|
||||
state = STATE_ALARM_DISARMED
|
||||
elif status == self._client.ARMED_AWAY:
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif status == self._client.ARMED_STAY:
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
elif status == self._client.TRIGGERED:
|
||||
state = STATE_ALARM_TRIGGERED
|
||||
else:
|
||||
state = None
|
||||
|
||||
self._state = state
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if self._validate_code(code):
|
||||
self._client.disarm()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if self._validate_code(code):
|
||||
self._client.arm_away()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
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
|
||||
10
homeassistant/components/alarm_control_panel/manifest.json
Normal file
10
homeassistant/components/alarm_control_panel/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "alarm_control_panel",
|
||||
"name": "Alarm control panel",
|
||||
"documentation": "https://www.home-assistant.io/components/alarm_control_panel",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@colinodell"
|
||||
]
|
||||
}
|
||||
@@ -1,324 +0,0 @@
|
||||
"""
|
||||
Support for manual alarms.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.manual/
|
||||
"""
|
||||
import copy
|
||||
import datetime
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.const import (
|
||||
CONF_CODE, CONF_DELAY_TIME, CONF_DISARM_AFTER_TRIGGER, CONF_NAME,
|
||||
CONF_PENDING_TIME, CONF_PLATFORM, CONF_TRIGGER_TIME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_CODE_TEMPLATE = 'code_template'
|
||||
|
||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
||||
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
|
||||
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
|
||||
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
|
||||
DEFAULT_DISARM_AFTER_TRIGGER = False
|
||||
|
||||
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED]
|
||||
|
||||
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_TRIGGERED]
|
||||
|
||||
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_DISARMED]
|
||||
|
||||
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
|
||||
ATTR_POST_PENDING_STATE = 'post_pending_state'
|
||||
|
||||
|
||||
def _state_validator(config):
|
||||
"""Validate the state."""
|
||||
config = copy.deepcopy(config)
|
||||
for state in SUPPORTED_PRETRIGGER_STATES:
|
||||
if CONF_DELAY_TIME not in config[state]:
|
||||
config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME]
|
||||
if CONF_TRIGGER_TIME not in config[state]:
|
||||
config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
if CONF_PENDING_TIME not in config[state]:
|
||||
config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME]
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def _state_schema(state):
|
||||
"""Validate the state."""
|
||||
schema = {}
|
||||
if state in SUPPORTED_PRETRIGGER_STATES:
|
||||
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
if state in SUPPORTED_PENDING_STATES:
|
||||
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
return vol.Schema(schema)
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema(vol.All({
|
||||
vol.Required(CONF_PLATFORM): 'manual',
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||
vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
|
||||
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
|
||||
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
|
||||
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_AWAY),
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_HOME),
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_NIGHT),
|
||||
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_CUSTOM_BYPASS),
|
||||
vol.Optional(STATE_ALARM_DISARMED, default={}):
|
||||
_state_schema(STATE_ALARM_DISARMED),
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
|
||||
_state_schema(STATE_ALARM_TRIGGERED),
|
||||
}, _state_validator))
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the manual alarm platform."""
|
||||
add_entities([ManualAlarm(
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
config.get(CONF_CODE),
|
||||
config.get(CONF_CODE_TEMPLATE),
|
||||
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
|
||||
config
|
||||
)])
|
||||
|
||||
|
||||
class ManualAlarm(alarm.AlarmControlPanel, RestoreEntity):
|
||||
"""
|
||||
Representation of an alarm status.
|
||||
|
||||
When armed, will be pending for 'pending_time', after that armed.
|
||||
When triggered, will be pending for the triggering state's 'delay_time'
|
||||
plus the triggered state's 'pending_time'.
|
||||
After that will be triggered for 'trigger_time', after that we return to
|
||||
the previous state or disarm if `disarm_after_trigger` is true.
|
||||
A trigger_time of zero disables the alarm_trigger service.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, name, code, code_template,
|
||||
disarm_after_trigger, config):
|
||||
"""Init the manual alarm panel."""
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
if code_template:
|
||||
self._code = code_template
|
||||
self._code.hass = hass
|
||||
else:
|
||||
self._code = code or None
|
||||
self._disarm_after_trigger = disarm_after_trigger
|
||||
self._previous_state = self._state
|
||||
self._state_ts = None
|
||||
|
||||
self._delay_time_by_state = {
|
||||
state: config[state][CONF_DELAY_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
self._trigger_time_by_state = {
|
||||
state: config[state][CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
self._pending_time_by_state = {
|
||||
state: config[state][CONF_PENDING_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES}
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._state == STATE_ALARM_TRIGGERED:
|
||||
if self._within_pending_time(self._state):
|
||||
return STATE_ALARM_PENDING
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
if (self._state_ts + self._pending_time(self._state) +
|
||||
trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
self._state = self._previous_state
|
||||
return self._state
|
||||
|
||||
if self._state in SUPPORTED_PENDING_STATES and \
|
||||
self._within_pending_time(self._state):
|
||||
return STATE_ALARM_PENDING
|
||||
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def _active_state(self):
|
||||
"""Get the current state."""
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
return self._previous_state
|
||||
return self._state
|
||||
|
||||
def _pending_time(self, state):
|
||||
"""Get the pending time."""
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
pending_time += self._delay_time_by_state[self._previous_state]
|
||||
return pending_time
|
||||
|
||||
def _within_pending_time(self, state):
|
||||
"""Get if the action is in the pending time window."""
|
||||
return self._state_ts + self._pending_time(state) > dt_util.utcnow()
|
||||
|
||||
@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
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, STATE_ALARM_DISARMED):
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_HOME):
|
||||
return
|
||||
|
||||
self._update_state(STATE_ALARM_ARMED_HOME)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_AWAY):
|
||||
return
|
||||
|
||||
self._update_state(STATE_ALARM_ARMED_AWAY)
|
||||
|
||||
def alarm_arm_night(self, code=None):
|
||||
"""Send arm night command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_NIGHT):
|
||||
return
|
||||
|
||||
self._update_state(STATE_ALARM_ARMED_NIGHT)
|
||||
|
||||
def alarm_arm_custom_bypass(self, code=None):
|
||||
"""Send arm custom bypass command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_CUSTOM_BYPASS):
|
||||
return
|
||||
|
||||
self._update_state(STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""
|
||||
Send alarm trigger command.
|
||||
|
||||
No code needed, a trigger time of zero for the current state
|
||||
disables the alarm.
|
||||
"""
|
||||
if not self._trigger_time_by_state[self._active_state]:
|
||||
return
|
||||
self._update_state(STATE_ALARM_TRIGGERED)
|
||||
|
||||
def _update_state(self, state):
|
||||
"""Update the state."""
|
||||
if self._state == state:
|
||||
return
|
||||
|
||||
self._previous_state = self._state
|
||||
self._state = state
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
pending_time = self._pending_time(state)
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time)
|
||||
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time + trigger_time)
|
||||
elif state in SUPPORTED_PENDING_STATES and pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
if self._code is None:
|
||||
return True
|
||||
if isinstance(self._code, str):
|
||||
alarm_code = self._code
|
||||
else:
|
||||
alarm_code = self._code.render(from_state=self._state,
|
||||
to_state=state)
|
||||
check = not alarm_code or code == alarm_code
|
||||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
return check
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
state_attr = {}
|
||||
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state
|
||||
state_attr[ATTR_POST_PENDING_STATE] = self._state
|
||||
|
||||
return state_attr
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
state = await self.async_get_last_state()
|
||||
if state:
|
||||
if state.state == STATE_ALARM_PENDING and \
|
||||
hasattr(state, 'attributes') and \
|
||||
state.attributes['pre_pending_state']:
|
||||
# If in pending state, we return to the pre_pending_state
|
||||
self._state = state.attributes['pre_pending_state']
|
||||
self._state_ts = dt_util.utcnow()
|
||||
else:
|
||||
self._state = state.state
|
||||
self._state_ts = state.last_updated
|
||||
@@ -1,366 +0,0 @@
|
||||
"""
|
||||
Support for manual alarms controllable via MQTT.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.manual_mqtt/
|
||||
"""
|
||||
import copy
|
||||
import datetime
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
|
||||
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME,
|
||||
CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
|
||||
from homeassistant.components import mqtt
|
||||
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.core import callback
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_CODE_TEMPLATE = 'code_template'
|
||||
|
||||
CONF_PAYLOAD_DISARM = 'payload_disarm'
|
||||
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
|
||||
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
|
||||
CONF_PAYLOAD_ARM_NIGHT = 'payload_arm_night'
|
||||
|
||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
||||
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
|
||||
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
|
||||
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
|
||||
DEFAULT_DISARM_AFTER_TRIGGER = False
|
||||
DEFAULT_ARM_AWAY = 'ARM_AWAY'
|
||||
DEFAULT_ARM_HOME = 'ARM_HOME'
|
||||
DEFAULT_ARM_NIGHT = 'ARM_NIGHT'
|
||||
DEFAULT_DISARM = 'DISARM'
|
||||
|
||||
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_TRIGGERED]
|
||||
|
||||
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_TRIGGERED]
|
||||
|
||||
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_DISARMED]
|
||||
|
||||
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
|
||||
ATTR_POST_PENDING_STATE = 'post_pending_state'
|
||||
|
||||
|
||||
def _state_validator(config):
|
||||
"""Validate the state."""
|
||||
config = copy.deepcopy(config)
|
||||
for state in SUPPORTED_PRETRIGGER_STATES:
|
||||
if CONF_DELAY_TIME not in config[state]:
|
||||
config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME]
|
||||
if CONF_TRIGGER_TIME not in config[state]:
|
||||
config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
if CONF_PENDING_TIME not in config[state]:
|
||||
config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME]
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def _state_schema(state):
|
||||
"""Validate the state."""
|
||||
schema = {}
|
||||
if state in SUPPORTED_PRETRIGGER_STATES:
|
||||
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
if state in SUPPORTED_PENDING_STATES:
|
||||
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
return vol.Schema(schema)
|
||||
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PLATFORM): 'manual_mqtt',
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||
vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
|
||||
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
|
||||
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
|
||||
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_AWAY),
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_HOME),
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_NIGHT),
|
||||
vol.Optional(STATE_ALARM_DISARMED, default={}):
|
||||
_state_schema(STATE_ALARM_DISARMED),
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
|
||||
_state_schema(STATE_ALARM_TRIGGERED),
|
||||
vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
|
||||
}), _state_validator))
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the manual MQTT alarm platform."""
|
||||
add_entities([ManualMQTTAlarm(
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
config.get(CONF_CODE),
|
||||
config.get(CONF_CODE_TEMPLATE),
|
||||
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
|
||||
config.get(mqtt.CONF_STATE_TOPIC),
|
||||
config.get(mqtt.CONF_COMMAND_TOPIC),
|
||||
config.get(mqtt.CONF_QOS),
|
||||
config.get(CONF_PAYLOAD_DISARM),
|
||||
config.get(CONF_PAYLOAD_ARM_HOME),
|
||||
config.get(CONF_PAYLOAD_ARM_AWAY),
|
||||
config.get(CONF_PAYLOAD_ARM_NIGHT),
|
||||
config)])
|
||||
|
||||
|
||||
class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
"""
|
||||
Representation of an alarm status.
|
||||
|
||||
When armed, will be pending for 'pending_time', after that armed.
|
||||
When triggered, will be pending for the triggering state's 'delay_time'
|
||||
plus the triggered state's 'pending_time'.
|
||||
After that will be triggered for 'trigger_time', after that we return to
|
||||
the previous state or disarm if `disarm_after_trigger` is true.
|
||||
A trigger_time of zero disables the alarm_trigger service.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, name, code, code_template, disarm_after_trigger,
|
||||
state_topic, command_topic, qos, payload_disarm,
|
||||
payload_arm_home, payload_arm_away, payload_arm_night,
|
||||
config):
|
||||
"""Init the manual MQTT alarm panel."""
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
if code_template:
|
||||
self._code = code_template
|
||||
self._code.hass = hass
|
||||
else:
|
||||
self._code = code or None
|
||||
self._disarm_after_trigger = disarm_after_trigger
|
||||
self._previous_state = self._state
|
||||
self._state_ts = None
|
||||
|
||||
self._delay_time_by_state = {
|
||||
state: config[state][CONF_DELAY_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
self._trigger_time_by_state = {
|
||||
state: config[state][CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
self._pending_time_by_state = {
|
||||
state: config[state][CONF_PENDING_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES}
|
||||
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
self._qos = qos
|
||||
self._payload_disarm = payload_disarm
|
||||
self._payload_arm_home = payload_arm_home
|
||||
self._payload_arm_away = payload_arm_away
|
||||
self._payload_arm_night = payload_arm_night
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._state == STATE_ALARM_TRIGGERED:
|
||||
if self._within_pending_time(self._state):
|
||||
return STATE_ALARM_PENDING
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
if (self._state_ts + self._pending_time(self._state) +
|
||||
trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
self._state = self._previous_state
|
||||
return self._state
|
||||
|
||||
if self._state in SUPPORTED_PENDING_STATES and \
|
||||
self._within_pending_time(self._state):
|
||||
return STATE_ALARM_PENDING
|
||||
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def _active_state(self):
|
||||
"""Get the current state."""
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
return self._previous_state
|
||||
return self._state
|
||||
|
||||
def _pending_time(self, state):
|
||||
"""Get the pending time."""
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
pending_time += self._delay_time_by_state[self._previous_state]
|
||||
return pending_time
|
||||
|
||||
def _within_pending_time(self, state):
|
||||
"""Get if the action is in the pending time window."""
|
||||
return self._state_ts + self._pending_time(state) > dt_util.utcnow()
|
||||
|
||||
@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
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, STATE_ALARM_DISARMED):
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_HOME):
|
||||
return
|
||||
|
||||
self._update_state(STATE_ALARM_ARMED_HOME)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_AWAY):
|
||||
return
|
||||
|
||||
self._update_state(STATE_ALARM_ARMED_AWAY)
|
||||
|
||||
def alarm_arm_night(self, code=None):
|
||||
"""Send arm night command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_NIGHT):
|
||||
return
|
||||
|
||||
self._update_state(STATE_ALARM_ARMED_NIGHT)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""
|
||||
Send alarm trigger command.
|
||||
|
||||
No code needed, a trigger time of zero for the current state
|
||||
disables the alarm.
|
||||
"""
|
||||
if not self._trigger_time_by_state[self._active_state]:
|
||||
return
|
||||
self._update_state(STATE_ALARM_TRIGGERED)
|
||||
|
||||
def _update_state(self, state):
|
||||
"""Update the state."""
|
||||
if self._state == state:
|
||||
return
|
||||
|
||||
self._previous_state = self._state
|
||||
self._state = state
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
pending_time = self._pending_time(state)
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time)
|
||||
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time + trigger_time)
|
||||
elif state in SUPPORTED_PENDING_STATES and pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
if self._code is None:
|
||||
return True
|
||||
if isinstance(self._code, str):
|
||||
alarm_code = self._code
|
||||
else:
|
||||
alarm_code = self._code.render(from_state=self._state,
|
||||
to_state=state)
|
||||
check = not alarm_code or code == alarm_code
|
||||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
return check
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
state_attr = {}
|
||||
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state
|
||||
state_attr[ATTR_POST_PENDING_STATE] = self._state
|
||||
|
||||
return state_attr
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to MQTT events."""
|
||||
async_track_state_change(
|
||||
self.hass, self.entity_id, self._async_state_changed_listener
|
||||
)
|
||||
|
||||
@callback
|
||||
def message_received(msg):
|
||||
"""Run when new MQTT message has been received."""
|
||||
if msg.payload == self._payload_disarm:
|
||||
self.async_alarm_disarm(self._code)
|
||||
elif msg.payload == self._payload_arm_home:
|
||||
self.async_alarm_arm_home(self._code)
|
||||
elif msg.payload == self._payload_arm_away:
|
||||
self.async_alarm_arm_away(self._code)
|
||||
elif msg.payload == self._payload_arm_night:
|
||||
self.async_alarm_arm_night(self._code)
|
||||
else:
|
||||
_LOGGER.warning("Received unexpected payload: %s", msg.payload)
|
||||
return
|
||||
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._command_topic, message_received, self._qos)
|
||||
|
||||
async def _async_state_changed_listener(self, entity_id, old_state,
|
||||
new_state):
|
||||
"""Publish state change to MQTT."""
|
||||
mqtt.async_publish(
|
||||
self.hass, self._state_topic, new_state.state, self._qos, True)
|
||||
@@ -1,107 +0,0 @@
|
||||
"""
|
||||
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()
|
||||
@@ -1,124 +0,0 @@
|
||||
"""
|
||||
Support for NX584 alarm control panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.nx584/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
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_PORT, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pynx584==0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_NAME = 'NX584'
|
||||
DEFAULT_PORT = 5007
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the NX584 platform."""
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
|
||||
url = 'http://{}:{}'.format(host, port)
|
||||
|
||||
try:
|
||||
add_entities([NX584Alarm(hass, url, name)])
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to NX584: %s", str(ex))
|
||||
return
|
||||
|
||||
|
||||
class NX584Alarm(alarm.AlarmControlPanel):
|
||||
"""Representation of a NX584-based alarm panel."""
|
||||
|
||||
def __init__(self, hass, url, name):
|
||||
"""Init the nx584 alarm panel."""
|
||||
from nx584 import client
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._url = url
|
||||
self._alarm = client.Client(self._url)
|
||||
# Do an initial list operation so that we will try to actually
|
||||
# talk to the API and trigger a requests exception for setup_platform()
|
||||
# to catch
|
||||
self._alarm.list_zones()
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more digits/characters."""
|
||||
return alarm.FORMAT_NUMBER
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Process new events from panel."""
|
||||
try:
|
||||
part = self._alarm.list_partitions()[0]
|
||||
zones = self._alarm.list_zones()
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to %(host)s: %(reason)s",
|
||||
dict(host=self._url, reason=ex))
|
||||
self._state = None
|
||||
zones = []
|
||||
except IndexError:
|
||||
_LOGGER.error("NX584 reports no partitions")
|
||||
self._state = None
|
||||
zones = []
|
||||
|
||||
bypassed = False
|
||||
for zone in zones:
|
||||
if zone['bypassed']:
|
||||
_LOGGER.debug("Zone %(zone)s is bypassed, assuming HOME",
|
||||
dict(zone=zone['number']))
|
||||
bypassed = True
|
||||
break
|
||||
|
||||
if not part['armed']:
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif bypassed:
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
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)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._alarm.arm('stay')
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._alarm.arm('exit')
|
||||
@@ -1,107 +0,0 @@
|
||||
"""
|
||||
Support for Vanderbilt (formerly Siemens) SPC alarm systems.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.spc/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.spc import (DATA_API, SIGNAL_UPDATE_ALARM)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_alarm_state(area):
|
||||
"""Get the alarm state."""
|
||||
from pyspcwebgw.const import AreaMode
|
||||
|
||||
if area.verified_alarm:
|
||||
return STATE_ALARM_TRIGGERED
|
||||
|
||||
mode_to_state = {
|
||||
AreaMode.UNSET: STATE_ALARM_DISARMED,
|
||||
AreaMode.PART_SET_A: STATE_ALARM_ARMED_HOME,
|
||||
AreaMode.PART_SET_B: STATE_ALARM_ARMED_NIGHT,
|
||||
AreaMode.FULL_SET: STATE_ALARM_ARMED_AWAY,
|
||||
}
|
||||
return mode_to_state.get(area.mode)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the SPC alarm control panel platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
api = hass.data[DATA_API]
|
||||
async_add_entities([SpcAlarm(area=area, api=api)
|
||||
for area in api.areas.values()])
|
||||
|
||||
|
||||
class SpcAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation of the SPC alarm panel."""
|
||||
|
||||
def __init__(self, area, api):
|
||||
"""Initialize the SPC alarm panel."""
|
||||
self._area = area
|
||||
self._api = api
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call for adding new entities."""
|
||||
async_dispatcher_connect(self.hass,
|
||||
SIGNAL_UPDATE_ALARM.format(self._area.id),
|
||||
self._update_callback)
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._area.name
|
||||
|
||||
@property
|
||||
def changed_by(self):
|
||||
"""Return the user the last change was triggered by."""
|
||||
return self._area.last_changed_by
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return _get_alarm_state(self._area)
|
||||
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
from pyspcwebgw.const import AreaMode
|
||||
await self._api.change_mode(area=self._area,
|
||||
new_mode=AreaMode.UNSET)
|
||||
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
from pyspcwebgw.const import AreaMode
|
||||
await self._api.change_mode(area=self._area,
|
||||
new_mode=AreaMode.PART_SET_A)
|
||||
|
||||
async def async_alarm_arm_night(self, code=None):
|
||||
"""Send arm home command."""
|
||||
from pyspcwebgw.const import AreaMode
|
||||
await self._api.change_mode(area=self._area,
|
||||
new_mode=AreaMode.PART_SET_B)
|
||||
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
from pyspcwebgw.const import AreaMode
|
||||
await self._api.change_mode(area=self._area,
|
||||
new_mode=AreaMode.FULL_SET)
|
||||
@@ -1,106 +0,0 @@
|
||||
"""
|
||||
Interfaces with TotalConnect alarm control panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.totalconnect/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, CONF_NAME,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.25']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Total Connect'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up a TotalConnect control panel."""
|
||||
name = config.get(CONF_NAME)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
total_connect = TotalConnect(name, username, password)
|
||||
add_entities([total_connect], True)
|
||||
|
||||
|
||||
class TotalConnect(alarm.AlarmControlPanel):
|
||||
"""Represent an TotalConnect status."""
|
||||
|
||||
def __init__(self, name, username, password):
|
||||
"""Initialize the TotalConnect status."""
|
||||
from total_connect_client import TotalConnectClient
|
||||
|
||||
_LOGGER.debug("Setting up TotalConnect...")
|
||||
self._name = name
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._state = None
|
||||
self._client = TotalConnectClient.TotalConnectClient(
|
||||
username, password)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Return the state of the device."""
|
||||
status = self._client.get_armed_status()
|
||||
|
||||
if status == self._client.DISARMED:
|
||||
state = STATE_ALARM_DISARMED
|
||||
elif status == self._client.ARMED_STAY:
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
elif status == self._client.ARMED_AWAY:
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif status == self._client.ARMED_STAY_NIGHT:
|
||||
state = STATE_ALARM_ARMED_NIGHT
|
||||
elif status == self._client.ARMED_CUSTOM_BYPASS:
|
||||
state = STATE_ALARM_ARMED_CUSTOM_BYPASS
|
||||
elif status == self._client.ARMING:
|
||||
state = STATE_ALARM_ARMING
|
||||
elif status == self._client.DISARMING:
|
||||
state = STATE_ALARM_DISARMING
|
||||
else:
|
||||
state = None
|
||||
|
||||
self._state = state
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._client.disarm()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._client.arm_stay()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._client.arm_away()
|
||||
|
||||
def alarm_arm_night(self, code=None):
|
||||
"""Send arm night command."""
|
||||
self._client.arm_stay_night()
|
||||
@@ -1,98 +0,0 @@
|
||||
"""
|
||||
Yale Smart Alarm client for interacting with the Yale Smart Alarm System API.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://www.home-assistant.io/components/alarm_control_panel.yale_smart_alarm
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanel, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, CONF_NAME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['yalesmartalarmclient==0.1.6']
|
||||
|
||||
CONF_AREA_ID = 'area_id'
|
||||
|
||||
DEFAULT_NAME = 'Yale Smart Alarm'
|
||||
|
||||
DEFAULT_AREA_ID = '1'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_AREA_ID, default=DEFAULT_AREA_ID): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the alarm platform."""
|
||||
name = config[CONF_NAME]
|
||||
username = config[CONF_USERNAME]
|
||||
password = config[CONF_PASSWORD]
|
||||
area_id = config[CONF_AREA_ID]
|
||||
|
||||
from yalesmartalarmclient.client import (
|
||||
YaleSmartAlarmClient, AuthenticationError)
|
||||
try:
|
||||
client = YaleSmartAlarmClient(username, password, area_id)
|
||||
except AuthenticationError:
|
||||
_LOGGER.error("Authentication failed. Check credentials")
|
||||
return
|
||||
|
||||
add_entities([YaleAlarmDevice(name, client)], True)
|
||||
|
||||
|
||||
class YaleAlarmDevice(AlarmControlPanel):
|
||||
"""Represent a Yale Smart Alarm."""
|
||||
|
||||
def __init__(self, name, client):
|
||||
"""Initialize the Yale Alarm Device."""
|
||||
self._name = name
|
||||
self._client = client
|
||||
self._state = None
|
||||
|
||||
from yalesmartalarmclient.client import (YALE_STATE_DISARM,
|
||||
YALE_STATE_ARM_PARTIAL,
|
||||
YALE_STATE_ARM_FULL)
|
||||
self._state_map = {
|
||||
YALE_STATE_DISARM: STATE_ALARM_DISARMED,
|
||||
YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME,
|
||||
YALE_STATE_ARM_FULL: STATE_ALARM_ARMED_AWAY
|
||||
}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Return the state of the device."""
|
||||
armed_status = self._client.get_armed_status()
|
||||
|
||||
self._state = self._state_map.get(armed_status)
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._client.disarm()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._client.arm_partial()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._client.arm_full()
|
||||
@@ -5,13 +5,11 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
|
||||
|
||||
REQUIREMENTS = ['alarmdecoder==1.13.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'alarmdecoder'
|
||||
@@ -20,7 +18,6 @@ DATA_AD = 'alarmdecoder'
|
||||
|
||||
CONF_DEVICE = 'device'
|
||||
CONF_DEVICE_BAUD = 'baudrate'
|
||||
CONF_DEVICE_HOST = 'host'
|
||||
CONF_DEVICE_PATH = 'path'
|
||||
CONF_DEVICE_PORT = 'port'
|
||||
CONF_DEVICE_TYPE = 'type'
|
||||
@@ -55,7 +52,7 @@ SIGNAL_REL_MESSAGE = 'alarmdecoder.rel_message'
|
||||
|
||||
DEVICE_SOCKET_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_DEVICE_TYPE): 'socket',
|
||||
vol.Optional(CONF_DEVICE_HOST, default=DEFAULT_DEVICE_HOST): cv.string,
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_DEVICE_HOST): cv.string,
|
||||
vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port})
|
||||
|
||||
DEVICE_SERIAL_SCHEMA = vol.Schema({
|
||||
@@ -165,7 +162,7 @@ def setup(hass, config):
|
||||
|
||||
controller = False
|
||||
if device_type == 'socket':
|
||||
host = device.get(CONF_DEVICE_HOST)
|
||||
host = device.get(CONF_HOST)
|
||||
port = device.get(CONF_DEVICE_PORT)
|
||||
controller = AlarmDecoder(SocketDevice(interface=(host, port)))
|
||||
elif device_type == 'serial':
|
||||
|
||||
@@ -4,15 +4,14 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarmdecoder import DATA_AD, SIGNAL_PANEL_MESSAGE
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from . import DATA_AD, SIGNAL_PANEL_MESSAGE
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_ALARM_TOGGLE_CHIME = 'alarmdecoder_alarm_toggle_chime'
|
||||
ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
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, CONF_ZONE_LOOP, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE,
|
||||
SIGNAL_RFX_MESSAGE, SIGNAL_REL_MESSAGE, CONF_RELAY_ADDR,
|
||||
CONF_RELAY_CHAN)
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
from . import (
|
||||
CONF_RELAY_ADDR, CONF_RELAY_CHAN, CONF_ZONE_LOOP, CONF_ZONE_NAME,
|
||||
CONF_ZONE_RFID, CONF_ZONE_TYPE, CONF_ZONES, SIGNAL_REL_MESSAGE,
|
||||
SIGNAL_RFX_MESSAGE, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE, ZONE_SCHEMA)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
10
homeassistant/components/alarmdecoder/manifest.json
Normal file
10
homeassistant/components/alarmdecoder/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "alarmdecoder",
|
||||
"name": "Alarmdecoder",
|
||||
"documentation": "https://www.home-assistant.io/components/alarmdecoder",
|
||||
"requirements": [
|
||||
"alarmdecoder==1.13.2"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
}
|
||||
@@ -2,12 +2,11 @@
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components.alarmdecoder import (SIGNAL_PANEL_MESSAGE)
|
||||
|
||||
from . import SIGNAL_PANEL_MESSAGE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up for AlarmDecoder sensor devices."""
|
||||
|
||||
0
homeassistant/components/alarmdecoder/services.yaml
Normal file
0
homeassistant/components/alarmdecoder/services.yaml
Normal file
1
homeassistant/components/alarmdotcom/__init__.py
Normal file
1
homeassistant/components/alarmdotcom/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""The alarmdotcom component."""
|
||||
118
homeassistant/components/alarmdotcom/alarm_control_panel.py
Normal file
118
homeassistant/components/alarmdotcom/alarm_control_panel.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""Interfaces with Alarm.com alarm control panels."""
|
||||
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_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Alarm.com'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
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,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up a Alarm.com control panel."""
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
alarmdotcom = AlarmDotCom(hass, name, code, username, password)
|
||||
await alarmdotcom.async_login()
|
||||
async_add_entities([alarmdotcom])
|
||||
|
||||
|
||||
class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
"""Representation of an Alarm.com status."""
|
||||
|
||||
def __init__(self, hass, name, code, username, password):
|
||||
"""Initialize the Alarm.com status."""
|
||||
from pyalarmdotcom import Alarmdotcom
|
||||
_LOGGER.debug('Setting up Alarm.com...')
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._websession = async_get_clientsession(self._hass)
|
||||
self._state = None
|
||||
self._alarm = Alarmdotcom(
|
||||
username, password, self._websession, hass.loop)
|
||||
|
||||
async def async_login(self):
|
||||
"""Login to Alarm.com."""
|
||||
await self._alarm.async_login()
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch the latest state."""
|
||||
await self._alarm.async_update()
|
||||
return self._alarm.state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the alarm."""
|
||||
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."""
|
||||
if self._alarm.state.lower() == 'disarmed':
|
||||
return STATE_ALARM_DISARMED
|
||||
if self._alarm.state.lower() == 'armed stay':
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
if self._alarm.state.lower() == 'armed away':
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
return None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'sensor_status': self._alarm.sensor_status
|
||||
}
|
||||
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if self._validate_code(code):
|
||||
await self._alarm.async_alarm_disarm()
|
||||
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm hom command."""
|
||||
if self._validate_code(code):
|
||||
await self._alarm.async_alarm_arm_home()
|
||||
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if self._validate_code(code):
|
||||
await self._alarm.async_alarm_arm_away()
|
||||
|
||||
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
|
||||
10
homeassistant/components/alarmdotcom/manifest.json
Normal file
10
homeassistant/components/alarmdotcom/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "alarmdotcom",
|
||||
"name": "Alarmdotcom",
|
||||
"documentation": "https://www.home-assistant.io/components/alarmdotcom",
|
||||
"requirements": [
|
||||
"pyalarmdotcom==0.3.2"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
}
|
||||
8
homeassistant/components/alert/manifest.json
Normal file
8
homeassistant/components/alert/manifest.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"domain": "alert",
|
||||
"name": "Alert",
|
||||
"documentation": "https://www.home-assistant.io/components/alert",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
}
|
||||
@@ -17,8 +17,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
CONF_FLASH_BRIEFINGS = 'flash_briefings'
|
||||
CONF_SMART_HOME = 'smart_home'
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
ALEXA_ENTITY_SCHEMA = vol.Schema({
|
||||
vol.Optional(smart_home.CONF_DESCRIPTION): cv.string,
|
||||
vol.Optional(smart_home.CONF_DISPLAY_CATEGORIES): cv.string,
|
||||
|
||||
10
homeassistant/components/alexa/manifest.json
Normal file
10
homeassistant/components/alexa/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "alexa",
|
||||
"name": "Alexa",
|
||||
"documentation": "https://www.home-assistant.io/components/alexa",
|
||||
"requirements": [],
|
||||
"dependencies": [
|
||||
"http"
|
||||
],
|
||||
"codeowners": []
|
||||
}
|
||||
0
homeassistant/components/alexa/services.yaml
Normal file
0
homeassistant/components/alexa/services.yaml
Normal file
@@ -65,6 +65,12 @@ API_THERMOSTAT_MODES = OrderedDict([
|
||||
(climate.STATE_DRY, 'OFF'),
|
||||
])
|
||||
|
||||
PERCENTAGE_FAN_MAP = {
|
||||
fan.SPEED_LOW: 33,
|
||||
fan.SPEED_MEDIUM: 66,
|
||||
fan.SPEED_HIGH: 100,
|
||||
}
|
||||
|
||||
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
|
||||
|
||||
CONF_DESCRIPTION = 'description'
|
||||
@@ -580,6 +586,26 @@ class _AlexaPercentageController(_AlexaInterface):
|
||||
def name(self):
|
||||
return 'Alexa.PercentageController'
|
||||
|
||||
def properties_supported(self):
|
||||
return [{'name': 'percentage'}]
|
||||
|
||||
def properties_retrievable(self):
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
if name != 'percentage':
|
||||
raise _UnsupportedProperty(name)
|
||||
|
||||
if self.entity.domain == fan.DOMAIN:
|
||||
speed = self.entity.attributes.get(fan.ATTR_SPEED)
|
||||
|
||||
return PERCENTAGE_FAN_MAP.get(speed, 0)
|
||||
|
||||
if self.entity.domain == cover.DOMAIN:
|
||||
return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION, 0)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
class _AlexaSpeaker(_AlexaInterface):
|
||||
"""Implements Alexa.Speaker.
|
||||
@@ -885,13 +911,17 @@ class _MediaPlayerCapabilities(_AlexaEntity):
|
||||
return [_DisplayCategory.TV]
|
||||
|
||||
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.const.SUPPORT_VOLUME_SET:
|
||||
yield _AlexaSpeaker(self.entity)
|
||||
|
||||
power_features = (media_player.SUPPORT_TURN_ON |
|
||||
media_player.SUPPORT_TURN_OFF)
|
||||
if supported & power_features:
|
||||
yield _AlexaPowerController(self.entity)
|
||||
|
||||
step_volume_features = (media_player.const.SUPPORT_VOLUME_MUTE |
|
||||
media_player.const.SUPPORT_VOLUME_STEP)
|
||||
if supported & step_volume_features:
|
||||
|
||||
1
homeassistant/components/alpha_vantage/__init__.py
Normal file
1
homeassistant/components/alpha_vantage/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""The alpha_vantage component."""
|
||||
12
homeassistant/components/alpha_vantage/manifest.json
Normal file
12
homeassistant/components/alpha_vantage/manifest.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "alpha_vantage",
|
||||
"name": "Alpha vantage",
|
||||
"documentation": "https://www.home-assistant.io/components/alpha_vantage",
|
||||
"requirements": [
|
||||
"alpha_vantage==2.1.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@fabaff"
|
||||
]
|
||||
}
|
||||
212
homeassistant/components/alpha_vantage/sensor.py
Normal file
212
homeassistant/components/alpha_vantage/sensor.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""Stock market information from Alpha Vantage."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_CLOSE = 'close'
|
||||
ATTR_HIGH = 'high'
|
||||
ATTR_LOW = 'low'
|
||||
|
||||
ATTRIBUTION = "Stock market information provided by Alpha Vantage"
|
||||
|
||||
CONF_FOREIGN_EXCHANGE = 'foreign_exchange'
|
||||
CONF_FROM = 'from'
|
||||
CONF_SYMBOL = 'symbol'
|
||||
CONF_SYMBOLS = 'symbols'
|
||||
CONF_TO = 'to'
|
||||
|
||||
ICONS = {
|
||||
'BTC': 'mdi:currency-btc',
|
||||
'EUR': 'mdi:currency-eur',
|
||||
'GBP': 'mdi:currency-gbp',
|
||||
'INR': 'mdi:currency-inr',
|
||||
'RUB': 'mdi:currency-rub',
|
||||
'TRY': 'mdi:currency-try',
|
||||
'USD': 'mdi:currency-usd',
|
||||
}
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
SYMBOL_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_SYMBOL): cv.string,
|
||||
vol.Optional(CONF_CURRENCY): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
CURRENCY_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_FROM): cv.string,
|
||||
vol.Required(CONF_TO): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Optional(CONF_FOREIGN_EXCHANGE):
|
||||
vol.All(cv.ensure_list, [CURRENCY_SCHEMA]),
|
||||
vol.Optional(CONF_SYMBOLS):
|
||||
vol.All(cv.ensure_list, [SYMBOL_SCHEMA]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Alpha Vantage sensor."""
|
||||
from alpha_vantage.timeseries import TimeSeries
|
||||
from alpha_vantage.foreignexchange import ForeignExchange
|
||||
|
||||
api_key = config.get(CONF_API_KEY)
|
||||
symbols = config.get(CONF_SYMBOLS, [])
|
||||
conversions = config.get(CONF_FOREIGN_EXCHANGE, [])
|
||||
|
||||
if not symbols and not conversions:
|
||||
msg = 'Warning: No symbols or currencies configured.'
|
||||
hass.components.persistent_notification.create(
|
||||
msg, 'Sensor alpha_vantage')
|
||||
_LOGGER.warning(msg)
|
||||
return
|
||||
|
||||
timeseries = TimeSeries(key=api_key)
|
||||
|
||||
dev = []
|
||||
for symbol in symbols:
|
||||
try:
|
||||
_LOGGER.debug("Configuring timeseries for symbols: %s",
|
||||
symbol[CONF_SYMBOL])
|
||||
timeseries.get_intraday(symbol[CONF_SYMBOL])
|
||||
except ValueError:
|
||||
_LOGGER.error(
|
||||
"API Key is not valid or symbol '%s' not known", symbol)
|
||||
dev.append(AlphaVantageSensor(timeseries, symbol))
|
||||
|
||||
forex = ForeignExchange(key=api_key)
|
||||
for conversion in conversions:
|
||||
from_cur = conversion.get(CONF_FROM)
|
||||
to_cur = conversion.get(CONF_TO)
|
||||
try:
|
||||
_LOGGER.debug("Configuring forex %s - %s", from_cur, to_cur)
|
||||
forex.get_currency_exchange_rate(
|
||||
from_currency=from_cur, to_currency=to_cur)
|
||||
except ValueError as error:
|
||||
_LOGGER.error(
|
||||
"API Key is not valid or currencies '%s'/'%s' not known",
|
||||
from_cur, to_cur)
|
||||
_LOGGER.debug(str(error))
|
||||
dev.append(AlphaVantageForeignExchange(forex, conversion))
|
||||
|
||||
add_entities(dev, True)
|
||||
_LOGGER.debug("Setup completed")
|
||||
|
||||
|
||||
class AlphaVantageSensor(Entity):
|
||||
"""Representation of a Alpha Vantage sensor."""
|
||||
|
||||
def __init__(self, timeseries, symbol):
|
||||
"""Initialize the sensor."""
|
||||
self._symbol = symbol[CONF_SYMBOL]
|
||||
self._name = symbol.get(CONF_NAME, self._symbol)
|
||||
self._timeseries = timeseries
|
||||
self.values = None
|
||||
self._unit_of_measurement = symbol.get(CONF_CURRENCY, self._symbol)
|
||||
self._icon = ICONS.get(symbol.get(CONF_CURRENCY, 'USD'))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self.values['1. open']
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self.values is not None:
|
||||
return {
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
ATTR_CLOSE: self.values['4. close'],
|
||||
ATTR_HIGH: self.values['2. high'],
|
||||
ATTR_LOW: self.values['3. low'],
|
||||
}
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return self._icon
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the states."""
|
||||
_LOGGER.debug("Requesting new data for symbol %s", self._symbol)
|
||||
all_values, _ = self._timeseries.get_intraday(self._symbol)
|
||||
self.values = next(iter(all_values.values()))
|
||||
_LOGGER.debug("Received new values for symbol %s", self._symbol)
|
||||
|
||||
|
||||
class AlphaVantageForeignExchange(Entity):
|
||||
"""Sensor for foreign exchange rates."""
|
||||
|
||||
def __init__(self, foreign_exchange, config):
|
||||
"""Initialize the sensor."""
|
||||
self._foreign_exchange = foreign_exchange
|
||||
self._from_currency = config.get(CONF_FROM)
|
||||
self._to_currency = config.get(CONF_TO)
|
||||
if CONF_NAME in config:
|
||||
self._name = config.get(CONF_NAME)
|
||||
else:
|
||||
self._name = '{}/{}'.format(self._to_currency, self._from_currency)
|
||||
self._unit_of_measurement = self._to_currency
|
||||
self._icon = ICONS.get(self._from_currency, 'USD')
|
||||
self.values = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return round(float(self.values['5. Exchange Rate']), 4)
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self.values is not None:
|
||||
return {
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
CONF_FROM: self._from_currency,
|
||||
CONF_TO: self._to_currency,
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the states."""
|
||||
_LOGGER.debug("Requesting new data for forex %s - %s",
|
||||
self._from_currency, self._to_currency)
|
||||
self.values, _ = self._foreign_exchange.get_currency_exchange_rate(
|
||||
from_currency=self._from_currency, to_currency=self._to_currency)
|
||||
_LOGGER.debug("Received new data for forex %s - %s",
|
||||
self._from_currency, self._to_currency)
|
||||
1
homeassistant/components/amazon_polly/__init__.py
Normal file
1
homeassistant/components/amazon_polly/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Support for Amazon Polly integration."""
|
||||
12
homeassistant/components/amazon_polly/manifest.json
Normal file
12
homeassistant/components/amazon_polly/manifest.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "amazon_polly",
|
||||
"name": "Amazon polly",
|
||||
"documentation": "https://www.home-assistant.io/components/amazon_polly",
|
||||
"requirements": [
|
||||
"boto3==1.9.16"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@robbiet480"
|
||||
]
|
||||
}
|
||||
202
homeassistant/components/amazon_polly/tts.py
Normal file
202
homeassistant/components/amazon_polly/tts.py
Normal file
@@ -0,0 +1,202 @@
|
||||
"""Support for the Amazon Polly text to speech service."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.tts import PLATFORM_SCHEMA, Provider
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_REGION = 'region_name'
|
||||
CONF_ACCESS_KEY_ID = 'aws_access_key_id'
|
||||
CONF_SECRET_ACCESS_KEY = 'aws_secret_access_key'
|
||||
CONF_PROFILE_NAME = 'profile_name'
|
||||
ATTR_CREDENTIALS = 'credentials'
|
||||
|
||||
DEFAULT_REGION = 'us-east-1'
|
||||
SUPPORTED_REGIONS = ['us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
|
||||
'ca-central-1', 'eu-west-1', 'eu-central-1', 'eu-west-2',
|
||||
'eu-west-3', 'ap-southeast-1', 'ap-southeast-2',
|
||||
'ap-northeast-2', 'ap-northeast-1', 'ap-south-1',
|
||||
'sa-east-1']
|
||||
|
||||
CONF_VOICE = 'voice'
|
||||
CONF_OUTPUT_FORMAT = 'output_format'
|
||||
CONF_SAMPLE_RATE = 'sample_rate'
|
||||
CONF_TEXT_TYPE = 'text_type'
|
||||
|
||||
SUPPORTED_VOICES = [
|
||||
'Zhiyu', # Chinese
|
||||
'Mads', 'Naja', # Danish
|
||||
'Ruben', 'Lotte', # Dutch
|
||||
'Russell', 'Nicole', # English Austrailian
|
||||
'Brian', 'Amy', 'Emma', # English
|
||||
'Aditi', 'Raveena', # English, Indian
|
||||
'Joey', 'Justin', 'Matthew', 'Ivy', 'Joanna', 'Kendra', 'Kimberly',
|
||||
'Salli', # English
|
||||
'Geraint', # English Welsh
|
||||
'Mathieu', 'Celine', 'Lea', # French
|
||||
'Chantal', # French Canadian
|
||||
'Hans', 'Marlene', 'Vicki', # German
|
||||
'Aditi', # Hindi
|
||||
'Karl', 'Dora', # Icelandic
|
||||
'Giorgio', 'Carla', 'Bianca', # Italian
|
||||
'Takumi', 'Mizuki', # Japanese
|
||||
'Seoyeon', # Korean
|
||||
'Liv', # Norwegian
|
||||
'Jacek', 'Jan', 'Ewa', 'Maja', # Polish
|
||||
'Ricardo', 'Vitoria', # Portuguese, Brazilian
|
||||
'Cristiano', 'Ines', # Portuguese, European
|
||||
'Carmen', # Romanian
|
||||
'Maxim', 'Tatyana', # Russian
|
||||
'Enrique', 'Conchita', 'Lucia', # Spanish European
|
||||
'Mia', # Spanish Mexican
|
||||
'Miguel', 'Penelope', # Spanish US
|
||||
'Astrid', # Swedish
|
||||
'Filiz', # Turkish
|
||||
'Gwyneth', # Welsh
|
||||
]
|
||||
|
||||
SUPPORTED_OUTPUT_FORMATS = ['mp3', 'ogg_vorbis', 'pcm']
|
||||
|
||||
SUPPORTED_SAMPLE_RATES = ['8000', '16000', '22050']
|
||||
|
||||
SUPPORTED_SAMPLE_RATES_MAP = {
|
||||
'mp3': ['8000', '16000', '22050'],
|
||||
'ogg_vorbis': ['8000', '16000', '22050'],
|
||||
'pcm': ['8000', '16000'],
|
||||
}
|
||||
|
||||
SUPPORTED_TEXT_TYPES = ['text', 'ssml']
|
||||
|
||||
CONTENT_TYPE_EXTENSIONS = {
|
||||
'audio/mpeg': 'mp3',
|
||||
'audio/ogg': 'ogg',
|
||||
'audio/pcm': 'pcm',
|
||||
}
|
||||
|
||||
DEFAULT_VOICE = 'Joanna'
|
||||
DEFAULT_OUTPUT_FORMAT = 'mp3'
|
||||
DEFAULT_TEXT_TYPE = 'text'
|
||||
|
||||
DEFAULT_SAMPLE_RATES = {
|
||||
'mp3': '22050',
|
||||
'ogg_vorbis': '22050',
|
||||
'pcm': '16000',
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_REGION, default=DEFAULT_REGION):
|
||||
vol.In(SUPPORTED_REGIONS),
|
||||
vol.Inclusive(CONF_ACCESS_KEY_ID, ATTR_CREDENTIALS): cv.string,
|
||||
vol.Inclusive(CONF_SECRET_ACCESS_KEY, ATTR_CREDENTIALS): cv.string,
|
||||
vol.Exclusive(CONF_PROFILE_NAME, ATTR_CREDENTIALS): cv.string,
|
||||
vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORTED_VOICES),
|
||||
vol.Optional(CONF_OUTPUT_FORMAT, default=DEFAULT_OUTPUT_FORMAT):
|
||||
vol.In(SUPPORTED_OUTPUT_FORMATS),
|
||||
vol.Optional(CONF_SAMPLE_RATE):
|
||||
vol.All(cv.string, vol.In(SUPPORTED_SAMPLE_RATES)),
|
||||
vol.Optional(CONF_TEXT_TYPE, default=DEFAULT_TEXT_TYPE):
|
||||
vol.In(SUPPORTED_TEXT_TYPES),
|
||||
})
|
||||
|
||||
|
||||
def get_engine(hass, config):
|
||||
"""Set up Amazon Polly speech component."""
|
||||
output_format = config.get(CONF_OUTPUT_FORMAT)
|
||||
sample_rate = config.get(
|
||||
CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATES[output_format])
|
||||
if sample_rate not in SUPPORTED_SAMPLE_RATES_MAP.get(output_format):
|
||||
_LOGGER.error("%s is not a valid sample rate for %s",
|
||||
sample_rate, output_format)
|
||||
return None
|
||||
|
||||
config[CONF_SAMPLE_RATE] = sample_rate
|
||||
|
||||
import boto3
|
||||
|
||||
profile = config.get(CONF_PROFILE_NAME)
|
||||
|
||||
if profile is not None:
|
||||
boto3.setup_default_session(profile_name=profile)
|
||||
|
||||
aws_config = {
|
||||
CONF_REGION: config.get(CONF_REGION),
|
||||
CONF_ACCESS_KEY_ID: config.get(CONF_ACCESS_KEY_ID),
|
||||
CONF_SECRET_ACCESS_KEY: config.get(CONF_SECRET_ACCESS_KEY),
|
||||
}
|
||||
|
||||
del config[CONF_REGION]
|
||||
del config[CONF_ACCESS_KEY_ID]
|
||||
del config[CONF_SECRET_ACCESS_KEY]
|
||||
|
||||
polly_client = boto3.client('polly', **aws_config)
|
||||
|
||||
supported_languages = []
|
||||
|
||||
all_voices = {}
|
||||
|
||||
all_voices_req = polly_client.describe_voices()
|
||||
|
||||
for voice in all_voices_req.get('Voices'):
|
||||
all_voices[voice.get('Id')] = voice
|
||||
if voice.get('LanguageCode') not in supported_languages:
|
||||
supported_languages.append(voice.get('LanguageCode'))
|
||||
|
||||
return AmazonPollyProvider(
|
||||
polly_client, config, supported_languages, all_voices)
|
||||
|
||||
|
||||
class AmazonPollyProvider(Provider):
|
||||
"""Amazon Polly speech api provider."""
|
||||
|
||||
def __init__(self, polly_client, config, supported_languages,
|
||||
all_voices):
|
||||
"""Initialize Amazon Polly provider for TTS."""
|
||||
self.client = polly_client
|
||||
self.config = config
|
||||
self.supported_langs = supported_languages
|
||||
self.all_voices = all_voices
|
||||
self.default_voice = self.config.get(CONF_VOICE)
|
||||
self.name = 'Amazon Polly'
|
||||
|
||||
@property
|
||||
def supported_languages(self):
|
||||
"""Return a list of supported languages."""
|
||||
return self.supported_langs
|
||||
|
||||
@property
|
||||
def default_language(self):
|
||||
"""Return the default language."""
|
||||
return self.all_voices.get(self.default_voice).get('LanguageCode')
|
||||
|
||||
@property
|
||||
def default_options(self):
|
||||
"""Return dict include default options."""
|
||||
return {CONF_VOICE: self.default_voice}
|
||||
|
||||
@property
|
||||
def supported_options(self):
|
||||
"""Return a list of supported options."""
|
||||
return [CONF_VOICE]
|
||||
|
||||
def get_tts_audio(self, message, language=None, options=None):
|
||||
"""Request TTS file from Polly."""
|
||||
voice_id = options.get(CONF_VOICE, self.default_voice)
|
||||
voice_in_dict = self.all_voices.get(voice_id)
|
||||
if language != voice_in_dict.get('LanguageCode'):
|
||||
_LOGGER.error("%s does not support the %s language",
|
||||
voice_id, language)
|
||||
return None, None
|
||||
|
||||
resp = self.client.synthesize_speech(
|
||||
OutputFormat=self.config[CONF_OUTPUT_FORMAT],
|
||||
SampleRate=self.config[CONF_SAMPLE_RATE],
|
||||
Text=message,
|
||||
TextType=self.config[CONF_TEXT_TYPE],
|
||||
VoiceId=voice_id
|
||||
)
|
||||
|
||||
return (CONTENT_TYPE_EXTENSIONS[resp.get('ContentType')],
|
||||
resp.get('AudioStream').read())
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"identifier_exists": "Application \u0438/\u0438\u043b\u0438 API \u043a\u043b\u044e\u0447\u044a\u0442 \u0432\u0435\u0447\u0435 \u0441\u0430 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d\u0438",
|
||||
"invalid_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447 \u0438/\u0438\u043b\u0438 Application \u043a\u043b\u044e\u0447",
|
||||
"no_devices": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043f\u0440\u043e\u0444\u0438\u043b\u0430"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API \u043a\u043b\u044e\u0447",
|
||||
"app_key": "Application \u043a\u043b\u044e\u0447"
|
||||
},
|
||||
"title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u0442\u0430 \u0441\u0438"
|
||||
}
|
||||
},
|
||||
"title": "\u0410\u0442\u043c\u043e\u0441\u0444\u0435\u0440\u043d\u0430 PWS"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,19 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"identifier_exists": "La clave API y/o la clave de aplicaci\u00f3n ya est\u00e1 registrada",
|
||||
"invalid_key": "Clave API y/o clave de aplicaci\u00f3n no v\u00e1lida",
|
||||
"no_devices": "No se han encontrado dispositivos en la cuenta"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Clave API",
|
||||
"app_key": "Clave de aplicaci\u00f3n"
|
||||
},
|
||||
"title": "Completa tu informaci\u00f3n"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Ambient PWS"
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"title": "Veuillez saisir vos informations"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Ambient PWS"
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
"api_key": "API \ud0a4",
|
||||
"app_key": "Application \ud0a4"
|
||||
},
|
||||
"title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694"
|
||||
"title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4 \uc785\ub825"
|
||||
}
|
||||
},
|
||||
"title": "Ambient PWS"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"no_devices": "\u0e44\u0e21\u0e48\u0e1e\u0e1a\u0e2d\u0e38\u0e1b\u0e01\u0e23\u0e13\u0e4c\u0e43\u0e14\u0e46 \u0e43\u0e19\u0e1a\u0e31\u0e0d\u0e0a\u0e35\u0e40\u0e25\u0e22"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"identifier_exists": "Application Key \u548c/\u6216 API Key \u5df2\u6ce8\u518c",
|
||||
"invalid_key": "\u65e0\u6548\u7684 API \u5bc6\u94a5\u548c/\u6216 Application Key",
|
||||
"no_devices": "\u6ca1\u6709\u5728\u5e10\u6237\u4e2d\u627e\u5230\u8bbe\u5907"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"app_key": "Application Key"
|
||||
},
|
||||
"title": "\u586b\u5199\u60a8\u7684\u4fe1\u606f"
|
||||
}
|
||||
},
|
||||
"title": "Ambient PWS"
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,6 @@ from .const import (
|
||||
ATTR_LAST_DATA, CONF_APP_KEY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE,
|
||||
TYPE_BINARY_SENSOR, TYPE_SENSOR)
|
||||
|
||||
REQUIREMENTS = ['aioambient==0.1.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_CONFIG = 'config'
|
||||
@@ -304,20 +302,33 @@ class AmbientStation:
|
||||
self.monitored_conditions = monitored_conditions
|
||||
self.stations = {}
|
||||
|
||||
async def ws_connect(self):
|
||||
"""Register handlers and connect to the websocket."""
|
||||
async def _attempt_connect(self):
|
||||
"""Attempt to connect to the socket (retrying later on fail)."""
|
||||
from aioambient.errors import WebsocketError
|
||||
|
||||
try:
|
||||
await self.client.websocket.connect()
|
||||
except WebsocketError as err:
|
||||
_LOGGER.error("Error with the websocket connection: %s", err)
|
||||
self._ws_reconnect_delay = min(
|
||||
2 * self._ws_reconnect_delay, 480)
|
||||
async_call_later(
|
||||
self._hass, self._ws_reconnect_delay, self.ws_connect)
|
||||
|
||||
async def ws_connect(self):
|
||||
"""Register handlers and connect to the websocket."""
|
||||
async def _ws_reconnect(event_time):
|
||||
"""Forcibly disconnect from and reconnect to the websocket."""
|
||||
_LOGGER.debug('Watchdog expired; forcing socket reconnection')
|
||||
await self.client.websocket.disconnect()
|
||||
await self.client.websocket.connect()
|
||||
await self._attempt_connect()
|
||||
|
||||
def on_connect():
|
||||
"""Define a handler to fire when the websocket is connected."""
|
||||
_LOGGER.info('Connected to websocket')
|
||||
_LOGGER.debug('Watchdog starting')
|
||||
if self._watchdog_listener:
|
||||
self._watchdog_listener()
|
||||
self._watchdog_listener = async_call_later(
|
||||
self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect)
|
||||
|
||||
@@ -381,15 +392,7 @@ class AmbientStation:
|
||||
self.client.websocket.on_disconnect(on_disconnect)
|
||||
self.client.websocket.on_subscribed(on_subscribed)
|
||||
|
||||
try:
|
||||
await self.client.websocket.connect()
|
||||
except WebsocketError as err:
|
||||
_LOGGER.error("Error with the websocket connection: %s", err)
|
||||
|
||||
self._ws_reconnect_delay = min(2 * self._ws_reconnect_delay, 480)
|
||||
|
||||
async_call_later(
|
||||
self._hass, self._ws_reconnect_delay, self.ws_connect)
|
||||
await self._attempt_connect()
|
||||
|
||||
async def ws_disconnect(self):
|
||||
"""Disconnect from the websocket."""
|
||||
@@ -411,6 +414,12 @@ class AmbientWeatherEntity(Entity):
|
||||
self._state = None
|
||||
self._station_name = station_name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get(
|
||||
self._sensor_type) is not None
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device registry information for this entity."""
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
"""Support for Ambient Weather Station binary sensors."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.ambient_station import (
|
||||
SENSOR_TYPES, TYPE_BATT1, TYPE_BATT10, TYPE_BATT2, TYPE_BATT3, TYPE_BATT4,
|
||||
TYPE_BATT5, TYPE_BATT6, TYPE_BATT7, TYPE_BATT8, TYPE_BATT9, TYPE_BATTOUT,
|
||||
AmbientWeatherEntity)
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import ATTR_NAME
|
||||
|
||||
from . import (
|
||||
SENSOR_TYPES, TYPE_BATT1, TYPE_BATT2, TYPE_BATT3, TYPE_BATT4, TYPE_BATT5,
|
||||
TYPE_BATT6, TYPE_BATT7, TYPE_BATT8, TYPE_BATT9, TYPE_BATT10, TYPE_BATTOUT,
|
||||
AmbientWeatherEntity)
|
||||
from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TYPE_BINARY_SENSOR
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['ambient_station']
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
|
||||
12
homeassistant/components/ambient_station/manifest.json
Normal file
12
homeassistant/components/ambient_station/manifest.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "ambient_station",
|
||||
"name": "Ambient station",
|
||||
"documentation": "https://www.home-assistant.io/components/ambient_station",
|
||||
"requirements": [
|
||||
"aioambient==0.3.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@bachya"
|
||||
]
|
||||
}
|
||||
@@ -1,16 +1,13 @@
|
||||
"""Support for Ambient Weather Station sensors."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.ambient_station import (
|
||||
SENSOR_TYPES, AmbientWeatherEntity)
|
||||
from homeassistant.const import ATTR_NAME
|
||||
|
||||
from . import SENSOR_TYPES, AmbientWeatherEntity
|
||||
from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TYPE_SENSOR
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['ambient_station']
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
|
||||
@@ -4,18 +4,14 @@ from datetime import timedelta
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
from requests.exceptions import ConnectionError as ConnectError
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
|
||||
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL,
|
||||
HTTP_BASIC_AUTHENTICATION)
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.2.5']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_AUTHENTICATION = 'authentication'
|
||||
@@ -27,6 +23,7 @@ DEFAULT_NAME = 'Amcrest Camera'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_RESOLUTION = 'high'
|
||||
DEFAULT_STREAM_SOURCE = 'snapshot'
|
||||
DEFAULT_ARGUMENTS = '-pred 1'
|
||||
TIMEOUT = 10
|
||||
|
||||
DATA_AMCREST = 'amcrest'
|
||||
@@ -52,9 +49,14 @@ STREAM_SOURCE_LIST = {
|
||||
'rtsp': 2,
|
||||
}
|
||||
|
||||
BINARY_SENSORS = {
|
||||
'motion_detected': 'Motion Detected'
|
||||
}
|
||||
|
||||
# Sensor types are defined like: Name, units, icon
|
||||
SENSOR_MOTION_DETECTOR = 'motion_detector'
|
||||
SENSORS = {
|
||||
'motion_detector': ['Motion Detected', None, 'mdi:run'],
|
||||
SENSOR_MOTION_DETECTOR: ['Motion Detected', None, 'mdi:run'],
|
||||
'sdcard': ['SD Used', '%', 'mdi:sd'],
|
||||
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
|
||||
}
|
||||
@@ -65,48 +67,74 @@ SWITCHES = {
|
||||
'motion_recording': ['Motion Recording', 'mdi:record-rec']
|
||||
}
|
||||
|
||||
|
||||
def _deprecated_sensors(value):
|
||||
if SENSOR_MOTION_DETECTOR in value:
|
||||
_LOGGER.warning(
|
||||
'sensors option %s is deprecated. '
|
||||
'Please remove from your configuration and '
|
||||
'use binary_sensors option motion_detected instead.',
|
||||
SENSOR_MOTION_DETECTOR)
|
||||
return value
|
||||
|
||||
|
||||
def _has_unique_names(value):
|
||||
names = [camera[CONF_NAME] for camera in value]
|
||||
vol.Schema(vol.Unique())(names)
|
||||
return value
|
||||
|
||||
|
||||
AMCREST_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
|
||||
vol.All(vol.In(AUTHENTICATION_LIST)),
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
|
||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS):
|
||||
cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Optional(CONF_BINARY_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]),
|
||||
vol.Optional(CONF_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)], _deprecated_sensors),
|
||||
vol.Optional(CONF_SWITCHES):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
|
||||
vol.All(vol.In(AUTHENTICATION_LIST)),
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
|
||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Optional(CONF_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
||||
vol.Optional(CONF_SWITCHES):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
})])
|
||||
DOMAIN: vol.All(cv.ensure_list, [AMCREST_SCHEMA], _has_unique_names)
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Amcrest IP Camera component."""
|
||||
from amcrest import AmcrestCamera
|
||||
from amcrest import AmcrestCamera, AmcrestError
|
||||
|
||||
hass.data[DATA_AMCREST] = {}
|
||||
hass.data.setdefault(DATA_AMCREST, {})
|
||||
amcrest_cams = config[DOMAIN]
|
||||
|
||||
for device in amcrest_cams:
|
||||
name = device[CONF_NAME]
|
||||
username = device[CONF_USERNAME]
|
||||
password = device[CONF_PASSWORD]
|
||||
|
||||
try:
|
||||
camera = AmcrestCamera(device.get(CONF_HOST),
|
||||
device.get(CONF_PORT),
|
||||
device.get(CONF_USERNAME),
|
||||
device.get(CONF_PASSWORD)).camera
|
||||
camera = AmcrestCamera(device[CONF_HOST],
|
||||
device[CONF_PORT],
|
||||
username,
|
||||
password).camera
|
||||
# pylint: disable=pointless-statement
|
||||
camera.current_time
|
||||
|
||||
except (ConnectError, ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
||||
except AmcrestError as ex:
|
||||
_LOGGER.error("Unable to connect to %s camera: %s", name, str(ex))
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
@@ -115,23 +143,19 @@ def setup(hass, config):
|
||||
notification_id=NOTIFICATION_ID)
|
||||
continue
|
||||
|
||||
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
|
||||
name = device.get(CONF_NAME)
|
||||
resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)]
|
||||
ffmpeg_arguments = device[CONF_FFMPEG_ARGUMENTS]
|
||||
resolution = RESOLUTION_LIST[device[CONF_RESOLUTION]]
|
||||
binary_sensors = device.get(CONF_BINARY_SENSORS)
|
||||
sensors = device.get(CONF_SENSORS)
|
||||
switches = device.get(CONF_SWITCHES)
|
||||
stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)]
|
||||
|
||||
username = device.get(CONF_USERNAME)
|
||||
password = device.get(CONF_PASSWORD)
|
||||
stream_source = STREAM_SOURCE_LIST[device[CONF_STREAM_SOURCE]]
|
||||
|
||||
# currently aiohttp only works with basic authentication
|
||||
# only valid for mjpeg streaming
|
||||
if username is not None and password is not None:
|
||||
if device.get(CONF_AUTHENTICATION) == HTTP_BASIC_AUTHENTICATION:
|
||||
authentication = aiohttp.BasicAuth(username, password)
|
||||
else:
|
||||
authentication = None
|
||||
if device[CONF_AUTHENTICATION] == HTTP_BASIC_AUTHENTICATION:
|
||||
authentication = aiohttp.BasicAuth(username, password)
|
||||
else:
|
||||
authentication = None
|
||||
|
||||
hass.data[DATA_AMCREST][name] = AmcrestDevice(
|
||||
camera, name, authentication, ffmpeg_arguments, stream_source,
|
||||
@@ -142,6 +166,13 @@ def setup(hass, config):
|
||||
CONF_NAME: name,
|
||||
}, config)
|
||||
|
||||
if binary_sensors:
|
||||
discovery.load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_BINARY_SENSORS: binary_sensors
|
||||
}, config)
|
||||
|
||||
if sensors:
|
||||
discovery.load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
@@ -156,7 +187,7 @@ def setup(hass, config):
|
||||
CONF_SWITCHES: switches
|
||||
}, config)
|
||||
|
||||
return True
|
||||
return len(hass.data[DATA_AMCREST]) >= 1
|
||||
|
||||
|
||||
class AmcrestDevice:
|
||||
|
||||
69
homeassistant/components/amcrest/binary_sensor.py
Normal file
69
homeassistant/components/amcrest/binary_sensor.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Suppoort for Amcrest IP camera binary sensors."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, DEVICE_CLASS_MOTION)
|
||||
from homeassistant.const import CONF_NAME, CONF_BINARY_SENSORS
|
||||
from . import DATA_AMCREST, BINARY_SENSORS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up a binary sensor for an Amcrest IP Camera."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
device_name = discovery_info[CONF_NAME]
|
||||
binary_sensors = discovery_info[CONF_BINARY_SENSORS]
|
||||
amcrest = hass.data[DATA_AMCREST][device_name]
|
||||
|
||||
amcrest_binary_sensors = []
|
||||
for sensor_type in binary_sensors:
|
||||
amcrest_binary_sensors.append(
|
||||
AmcrestBinarySensor(amcrest.name, amcrest.device, sensor_type))
|
||||
|
||||
async_add_devices(amcrest_binary_sensors, True)
|
||||
|
||||
|
||||
class AmcrestBinarySensor(BinarySensorDevice):
|
||||
"""Binary sensor for Amcrest camera."""
|
||||
|
||||
def __init__(self, name, camera, sensor_type):
|
||||
"""Initialize entity."""
|
||||
self._name = '{} {}'.format(name, BINARY_SENSORS[sensor_type])
|
||||
self._camera = camera
|
||||
self._sensor_type = sensor_type
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return entity name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return if entity is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return device class."""
|
||||
return DEVICE_CLASS_MOTION
|
||||
|
||||
def update(self):
|
||||
"""Update entity."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
_LOGGER.debug('Pulling data from %s binary sensor', self._name)
|
||||
|
||||
try:
|
||||
self._state = self._camera.is_motion_detected
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not update %s binary sensor due to error: %s',
|
||||
self.name, error)
|
||||
@@ -2,19 +2,15 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from requests import RequestException
|
||||
from urllib3.exceptions import ReadTimeoutError
|
||||
|
||||
from homeassistant.components.amcrest import (
|
||||
DATA_AMCREST, STREAM_SOURCE_LIST, TIMEOUT)
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.camera import (
|
||||
Camera, SUPPORT_ON_OFF, SUPPORT_STREAM)
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_aiohttp_proxy_web,
|
||||
async_aiohttp_proxy_stream)
|
||||
async_aiohttp_proxy_stream, async_aiohttp_proxy_web,
|
||||
async_get_clientsession)
|
||||
|
||||
DEPENDENCIES = ['amcrest', 'ffmpeg']
|
||||
from . import DATA_AMCREST, STREAM_SOURCE_LIST, TIMEOUT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,8 +26,6 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
|
||||
async_add_entities([AmcrestCam(hass, amcrest)], True)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AmcrestCam(Camera):
|
||||
"""An implementation of an Amcrest IP camera."""
|
||||
@@ -41,25 +35,33 @@ class AmcrestCam(Camera):
|
||||
super(AmcrestCam, self).__init__()
|
||||
self._name = amcrest.name
|
||||
self._camera = amcrest.device
|
||||
self._base_url = self._camera.get_base_url()
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = amcrest.ffmpeg_arguments
|
||||
self._stream_source = amcrest.stream_source
|
||||
self._resolution = amcrest.resolution
|
||||
self._token = self._auth = amcrest.authentication
|
||||
self._is_recording = False
|
||||
self._model = None
|
||||
self._snapshot_lock = asyncio.Lock()
|
||||
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
if not self.is_on:
|
||||
_LOGGER.error(
|
||||
'Attempt to take snaphot when %s camera is off', self.name)
|
||||
return None
|
||||
async with self._snapshot_lock:
|
||||
try:
|
||||
# Send the request to snap a picture and return raw jpg data
|
||||
response = await self.hass.async_add_executor_job(
|
||||
self._camera.snapshot, self._resolution)
|
||||
return response.data
|
||||
except (RequestException, ReadTimeoutError, ValueError) as error:
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not get camera image due to error %s', error)
|
||||
'Could not get image from %s camera due to error: %s',
|
||||
self.name, error)
|
||||
return None
|
||||
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
@@ -79,7 +81,7 @@ class AmcrestCam(Camera):
|
||||
self.hass, request, stream_coro)
|
||||
|
||||
# streaming via ffmpeg
|
||||
from haffmpeg import CameraMjpeg
|
||||
from haffmpeg.camera import CameraMjpeg
|
||||
|
||||
streaming_url = self._camera.rtsp_url(typeno=self._resolution)
|
||||
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
|
||||
@@ -87,18 +89,97 @@ class AmcrestCam(Camera):
|
||||
streaming_url, extra_cmd=self._ffmpeg_arguments)
|
||||
|
||||
try:
|
||||
stream_reader = await stream.get_reader()
|
||||
return await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
self.hass, request, stream_reader,
|
||||
self._ffmpeg.ffmpeg_stream_content_type)
|
||||
finally:
|
||||
await stream.close()
|
||||
|
||||
# Entity property overrides
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return supported features."""
|
||||
return SUPPORT_ON_OFF | SUPPORT_STREAM
|
||||
|
||||
# Camera property overrides
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
"""Return true if the device is recording."""
|
||||
return self._is_recording
|
||||
|
||||
@property
|
||||
def brand(self):
|
||||
"""Return the camera brand."""
|
||||
return 'Amcrest'
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Return the camera model."""
|
||||
return self._model
|
||||
|
||||
@property
|
||||
def stream_source(self):
|
||||
"""Return the source of the stream."""
|
||||
return self._camera.rtsp_url(typeno=self._resolution)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if on."""
|
||||
return self.is_streaming
|
||||
|
||||
# Other Entity method overrides
|
||||
|
||||
def update(self):
|
||||
"""Update entity status."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
_LOGGER.debug('Pulling data from %s camera', self.name)
|
||||
if self._model is None:
|
||||
try:
|
||||
self._model = self._camera.device_type.split('=')[-1].strip()
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not get %s camera model due to error: %s',
|
||||
self.name, error)
|
||||
self._model = ''
|
||||
try:
|
||||
self.is_streaming = self._camera.video_enabled
|
||||
self._is_recording = self._camera.record_mode == 'Manual'
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not get %s camera attributes due to error: %s',
|
||||
self.name, error)
|
||||
|
||||
# Other Camera method overrides
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off camera."""
|
||||
self._enable_video_stream(False)
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn on camera."""
|
||||
self._enable_video_stream(True)
|
||||
|
||||
# Utility methods
|
||||
|
||||
def _enable_video_stream(self, enable):
|
||||
"""Enable or disable camera video stream."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._camera.video_enabled = enable
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera video stream due to error: %s',
|
||||
'enable' if enable else 'disable', self.name, error)
|
||||
else:
|
||||
self.is_streaming = enable
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
12
homeassistant/components/amcrest/manifest.json
Normal file
12
homeassistant/components/amcrest/manifest.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "amcrest",
|
||||
"name": "Amcrest",
|
||||
"documentation": "https://www.home-assistant.io/components/amcrest",
|
||||
"requirements": [
|
||||
"amcrest==1.3.0"
|
||||
],
|
||||
"dependencies": [
|
||||
"ffmpeg"
|
||||
],
|
||||
"codeowners": []
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user