mirror of
https://github.com/home-assistant/core.git
synced 2025-11-03 07:59:30 +00:00
Compare commits
567 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ad495070d | ||
|
|
84719d944a | ||
|
|
4ca588deae | ||
|
|
325001933d | ||
|
|
acc9fd0382 | ||
|
|
ca89d6184c | ||
|
|
2bfe7aa219 | ||
|
|
78ffb6f3e6 | ||
|
|
c08862679d | ||
|
|
50db622689 | ||
|
|
9303a56d8f | ||
|
|
6667138b73 | ||
|
|
d9852bc75d | ||
|
|
6aeccf0330 | ||
|
|
f8572c1d71 | ||
|
|
e2e001d042 | ||
|
|
e3307213b1 | ||
|
|
84baaa324c | ||
|
|
42ee8eef50 | ||
|
|
3fef9a93cf | ||
|
|
4b256f3466 | ||
|
|
bebfc3d16e | ||
|
|
fd3902f7e7 | ||
|
|
dfb992adb2 | ||
|
|
a252065f99 | ||
|
|
6947f8cb2e | ||
|
|
85dfea1642 | ||
|
|
015c8811a5 | ||
|
|
d9c78b77cb | ||
|
|
1b543cf538 | ||
|
|
9fb8144031 | ||
|
|
f2033c418f | ||
|
|
aa266cb630 | ||
|
|
9a5d783537 | ||
|
|
5800b57791 | ||
|
|
c840771c0a | ||
|
|
9678752480 | ||
|
|
31b2f331db | ||
|
|
0ba54ee9b7 | ||
|
|
5c86a51b45 | ||
|
|
9debbfb1a8 | ||
|
|
97b671171b | ||
|
|
179fb0f3b5 | ||
|
|
96b7bb625d | ||
|
|
afeb13d980 | ||
|
|
6e1728542e | ||
|
|
9438dd1cbd | ||
|
|
0194905e97 | ||
|
|
25505dc1d4 | ||
|
|
ce219ac6c7 | ||
|
|
fa20957e01 | ||
|
|
7959c04d1e | ||
|
|
e6d7f6ed71 | ||
|
|
144b530045 | ||
|
|
39ba99005a | ||
|
|
f867b025e5 | ||
|
|
c928f82cbf | ||
|
|
9d7aa8f05d | ||
|
|
02f927ae2d | ||
|
|
1d022522cd | ||
|
|
bad9ac5395 | ||
|
|
e9f561e7ab | ||
|
|
14d169558f | ||
|
|
ca2a68217d | ||
|
|
0a9a8ecc4e | ||
|
|
6cef850497 | ||
|
|
66af4bd011 | ||
|
|
03253f4598 | ||
|
|
aa5d8e5a81 | ||
|
|
206029eadc | ||
|
|
958c5ecbfe | ||
|
|
3d79bf2bfe | ||
|
|
1de0a0bbb9 | ||
|
|
8d22479d24 | ||
|
|
7f7435f003 | ||
|
|
d2eb5bb0f3 | ||
|
|
085303c349 | ||
|
|
9ac6f906ff | ||
|
|
f995ab9d54 | ||
|
|
77f595c9a4 | ||
|
|
8d0b1588be | ||
|
|
70c5c82541 | ||
|
|
bf910ef383 | ||
|
|
99c49c0993 | ||
|
|
f6e6c21ba6 | ||
|
|
41b7f5ab1c | ||
|
|
c5bd6b3d6b | ||
|
|
9e96397e6a | ||
|
|
f207e01510 | ||
|
|
6b3bb3347b | ||
|
|
806903ffe0 | ||
|
|
fdf1fa48e3 | ||
|
|
636077c74d | ||
|
|
e047e4dcff | ||
|
|
eae306c3f1 | ||
|
|
fbd7c72283 | ||
|
|
fc58746bc3 | ||
|
|
9ae878d8f2 | ||
|
|
afe9fc221e | ||
|
|
eb912be47a | ||
|
|
5c346e8fb6 | ||
|
|
8d388c5e79 | ||
|
|
314574fc84 | ||
|
|
e356d0bcda | ||
|
|
f991ec15f2 | ||
|
|
d7d83c683d | ||
|
|
ff867a7d57 | ||
|
|
1282370ccb | ||
|
|
eebd094423 | ||
|
|
57bd4185d4 | ||
|
|
91ba35c68e | ||
|
|
a99e15343c | ||
|
|
4583638b92 | ||
|
|
10a1b156e3 | ||
|
|
a8286535eb | ||
|
|
05146badf1 | ||
|
|
c483e4479e | ||
|
|
4a70c725b4 | ||
|
|
33ed017851 | ||
|
|
fffc4dd3fd | ||
|
|
e072981295 | ||
|
|
5d983d0b61 | ||
|
|
727f667cbc | ||
|
|
1b4fc2ae8d | ||
|
|
5b0d1415ad | ||
|
|
7818c98c67 | ||
|
|
e12222697c | ||
|
|
58f28f177d | ||
|
|
6030e419c5 | ||
|
|
8d2a784831 | ||
|
|
5dc841ecae | ||
|
|
edf34eea94 | ||
|
|
a303f67d3b | ||
|
|
1b5f526e09 | ||
|
|
03a0a3572b | ||
|
|
297d24c5b0 | ||
|
|
c8cf06b8b7 | ||
|
|
49d6d7c656 | ||
|
|
96fd874090 | ||
|
|
c9703872e2 | ||
|
|
2f5d7d4522 | ||
|
|
7716e8fb68 | ||
|
|
c2fc8a0d61 | ||
|
|
9be384690a | ||
|
|
692eeb3687 | ||
|
|
213c91ae73 | ||
|
|
36b1a89f93 | ||
|
|
584bfbaa76 | ||
|
|
0f140751b2 | ||
|
|
6b359c95da | ||
|
|
1fec64a1b3 | ||
|
|
70ed58a78d | ||
|
|
6aa9844f8f | ||
|
|
7a4238095d | ||
|
|
177594f02c | ||
|
|
cf89f45697 | ||
|
|
2dc78e6f0c | ||
|
|
9da74dda43 | ||
|
|
18149dcb8c | ||
|
|
3f841a36a5 | ||
|
|
80ae02cc49 | ||
|
|
421b2962c6 | ||
|
|
bde5a9ef01 | ||
|
|
b79886ad85 | ||
|
|
94a2fd542e | ||
|
|
6fa8556033 | ||
|
|
19cfa8cf22 | ||
|
|
a859997190 | ||
|
|
6f9860b25e | ||
|
|
f083abbed1 | ||
|
|
4dd8423b9b | ||
|
|
8ba2ab567e | ||
|
|
3d821b9148 | ||
|
|
04720175b9 | ||
|
|
93ad7b2e45 | ||
|
|
128ce589e1 | ||
|
|
9b21774392 | ||
|
|
eaf4a75402 | ||
|
|
a1a6d4a631 | ||
|
|
de1fd5a7fa | ||
|
|
9b12dd66e4 | ||
|
|
0d96095646 | ||
|
|
45085dd97f | ||
|
|
b2a1204bc5 | ||
|
|
990a9e80a2 | ||
|
|
0ffcc197d4 | ||
|
|
1a051f038d | ||
|
|
1e22c8daca | ||
|
|
b8cbd39985 | ||
|
|
3508622e3b | ||
|
|
b8f6d824fd | ||
|
|
e687848152 | ||
|
|
2a9fd9ae26 | ||
|
|
3ec4070d8c | ||
|
|
6f8038992c | ||
|
|
5c9a58f3e6 | ||
|
|
d34214ad32 | ||
|
|
2b7021407c | ||
|
|
03cd4480df | ||
|
|
910825580e | ||
|
|
6d40980de1 | ||
|
|
d4f79fc88a | ||
|
|
2d724f5cc9 | ||
|
|
c8d479e594 | ||
|
|
34f6245e74 | ||
|
|
e9ea5c2ccb | ||
|
|
4347a0f6b7 | ||
|
|
5888e32360 | ||
|
|
4214a354a7 | ||
|
|
61ed604fda | ||
|
|
f17259c705 | ||
|
|
d5533cc10f | ||
|
|
e6c5cc92d1 | ||
|
|
0caa27094e | ||
|
|
01578f78f1 | ||
|
|
369afd7ddd | ||
|
|
281445917b | ||
|
|
df6846344d | ||
|
|
05960fa29c | ||
|
|
068749bcbe | ||
|
|
f21f32778f | ||
|
|
8ef3c6d4d3 | ||
|
|
c7a78ed522 | ||
|
|
45adb5c9c7 | ||
|
|
4004867eda | ||
|
|
118d3bc11c | ||
|
|
0e9d71f232 | ||
|
|
b552fbe312 | ||
|
|
07b7d7b074 | ||
|
|
fb7343ecd2 | ||
|
|
e51925fc58 | ||
|
|
d507adf13d | ||
|
|
c5230f7585 | ||
|
|
cc13713abd | ||
|
|
f019e2a204 | ||
|
|
07126266dd | ||
|
|
c26af22edd | ||
|
|
c384adeef4 | ||
|
|
7f0953766b | ||
|
|
ce1974b014 | ||
|
|
8822f0140d | ||
|
|
45c041884a | ||
|
|
1ecb3de643 | ||
|
|
f0f6787bf9 | ||
|
|
3e788aa1d6 | ||
|
|
f4016b4aad | ||
|
|
07ee3b2eb9 | ||
|
|
6e7a7ba4a0 | ||
|
|
7181d639fd | ||
|
|
0f174250cc | ||
|
|
16a27c3f2d | ||
|
|
b253c7499d | ||
|
|
264e70922b | ||
|
|
482cb0146a | ||
|
|
02d8731a61 | ||
|
|
102beaa044 | ||
|
|
6611d585e6 | ||
|
|
f386088def | ||
|
|
c07c557298 | ||
|
|
1f551e5f6f | ||
|
|
b60c815cde | ||
|
|
074142400a | ||
|
|
d8a219fe0e | ||
|
|
a0230482bd | ||
|
|
b9a72034f9 | ||
|
|
bf649e373c | ||
|
|
73aadbe8bc | ||
|
|
e2afeca4fd | ||
|
|
0dfa5f9ffd | ||
|
|
b331b081f0 | ||
|
|
cf03e42773 | ||
|
|
f87209605b | ||
|
|
3e59e7f347 | ||
|
|
3dd1d3c418 | ||
|
|
8328ea6bd7 | ||
|
|
9c1bbd1d9d | ||
|
|
8500244f8c | ||
|
|
2efc1de349 | ||
|
|
4796d674e6 | ||
|
|
4fe0cd76f8 | ||
|
|
c0f9ccfdbb | ||
|
|
b9fda078a4 | ||
|
|
e24d56aa5b | ||
|
|
8da600adff | ||
|
|
9712bbc91c | ||
|
|
fca0891320 | ||
|
|
1d2c5cb53c | ||
|
|
c309bd9ff0 | ||
|
|
2f416b15c5 | ||
|
|
85cd4ad022 | ||
|
|
c8690865ec | ||
|
|
8d8d2b6de9 | ||
|
|
97b5a38cb1 | ||
|
|
8fc30569a9 | ||
|
|
0702407fac | ||
|
|
6e34015420 | ||
|
|
df9a9a1fec | ||
|
|
9761c504eb | ||
|
|
b30afde8ab | ||
|
|
6130831a43 | ||
|
|
3338f5c9b4 | ||
|
|
592e99947d | ||
|
|
7331eb1f71 | ||
|
|
f434e24252 | ||
|
|
b79d71065c | ||
|
|
0a75a2c080 | ||
|
|
41357965de | ||
|
|
1e6babe796 | ||
|
|
04b680d9d0 | ||
|
|
5267635d2c | ||
|
|
a6b898d702 | ||
|
|
5c413eb497 | ||
|
|
21575938ef | ||
|
|
a7ef1eabb0 | ||
|
|
6124a6f7e5 | ||
|
|
d4ae73ce38 | ||
|
|
2fbf29cda6 | ||
|
|
3c5abcc71a | ||
|
|
447440adc3 | ||
|
|
daa1d103d4 | ||
|
|
58cde6b497 | ||
|
|
741d0fd09b | ||
|
|
bc9548fdaf | ||
|
|
35e9505ad5 | ||
|
|
38aa7d2c95 | ||
|
|
55a7ea6cc5 | ||
|
|
04bca7be6b | ||
|
|
8180797d2f | ||
|
|
0fe573ecc0 | ||
|
|
185595c113 | ||
|
|
ffdf48b15a | ||
|
|
fa4264be3f | ||
|
|
c7a34d0222 | ||
|
|
e054d68565 | ||
|
|
f3925b7ede | ||
|
|
d1e44e35df | ||
|
|
0fe21f2015 | ||
|
|
75abfd49ef | ||
|
|
f0c582ebbd | ||
|
|
7ff77936ad | ||
|
|
44d0d0624b | ||
|
|
a8c7804db2 | ||
|
|
beb678e259 | ||
|
|
d9d5c91adc | ||
|
|
19aee50bbc | ||
|
|
bb6300efe3 | ||
|
|
dd53434742 | ||
|
|
db2904624a | ||
|
|
d3cbd5b5e4 | ||
|
|
f9205d0ccc | ||
|
|
127cc5f942 | ||
|
|
ab60235811 | ||
|
|
7faa061b29 | ||
|
|
c7d49a0c6a | ||
|
|
ac731a817a | ||
|
|
f0a34ddf46 | ||
|
|
918ce74b26 | ||
|
|
e8f496c016 | ||
|
|
9b57075c3b | ||
|
|
09cd302b46 | ||
|
|
bf25b74bf1 | ||
|
|
3269da16f6 | ||
|
|
0a428868fe | ||
|
|
8cfc316e06 | ||
|
|
31e3c563b5 | ||
|
|
f28ca34307 | ||
|
|
09296b4fd4 | ||
|
|
0b9302ac3d | ||
|
|
5b9d01139d | ||
|
|
581b16e9fa | ||
|
|
1c4367e5a9 | ||
|
|
b0843f4a38 | ||
|
|
24060e0fb5 | ||
|
|
5ded0dd3fa | ||
|
|
09012e7baa | ||
|
|
75a2c057f2 | ||
|
|
603e2cd961 | ||
|
|
cfaaae661a | ||
|
|
41f0066e76 | ||
|
|
407e0c58f9 | ||
|
|
d71424f285 | ||
|
|
6a6a999833 | ||
|
|
7612703092 | ||
|
|
5d5f073cff | ||
|
|
1d70005b01 | ||
|
|
b4e2a0ef84 | ||
|
|
2aee31ec6a | ||
|
|
8d775caaac | ||
|
|
4c4f0e38d4 | ||
|
|
5e3e730496 | ||
|
|
84f778d23c | ||
|
|
75f53b2799 | ||
|
|
e08f2ad18d | ||
|
|
5aa9a1a7c2 | ||
|
|
5e045f3df2 | ||
|
|
471a26bde1 | ||
|
|
2245ee98e3 | ||
|
|
0ecf152153 | ||
|
|
5529bcc114 | ||
|
|
b4a7980084 | ||
|
|
0f49a9cb7b | ||
|
|
2f45a7e3b9 | ||
|
|
b60c7ce479 | ||
|
|
41d9bd42af | ||
|
|
2fecc7d5a4 | ||
|
|
687bbce900 | ||
|
|
54c34bb224 | ||
|
|
37badbbf09 | ||
|
|
b09f5b6743 | ||
|
|
2dbe6d3289 | ||
|
|
300d1f44a6 | ||
|
|
7458f1f6ef | ||
|
|
26bf1b2173 | ||
|
|
f1b2622d78 | ||
|
|
5efe089699 | ||
|
|
b6a13262da | ||
|
|
148860587c | ||
|
|
6be798bffc | ||
|
|
1dbfa8f3be | ||
|
|
96fb311f6b | ||
|
|
bdc95e76d0 | ||
|
|
c174b83f54 | ||
|
|
bf050adcf3 | ||
|
|
c2e7445271 | ||
|
|
f25183ba30 | ||
|
|
d6f6273ac2 | ||
|
|
61ea6256c6 | ||
|
|
08c36e0089 | ||
|
|
8fe95f4bab | ||
|
|
606dbb85d2 | ||
|
|
b84ba93c42 | ||
|
|
5dbf58d67f | ||
|
|
d038d2426b | ||
|
|
b5725f8f19 | ||
|
|
eefb9406c2 | ||
|
|
c229a314c6 | ||
|
|
7d5c1ede72 | ||
|
|
7a6acca6bb | ||
|
|
9d67c9feb6 | ||
|
|
cef7ce11ad | ||
|
|
e182b95921 | ||
|
|
d2e0c6dbc2 | ||
|
|
39932d132d | ||
|
|
7e8f2d72b6 | ||
|
|
4816a24b3c | ||
|
|
3d91d76d3d | ||
|
|
5376e15286 | ||
|
|
86b017e2f0 | ||
|
|
c216ac7260 | ||
|
|
de6fdb09f4 | ||
|
|
e3e7fb5ff6 | ||
|
|
6fb5b8467b | ||
|
|
24766df179 | ||
|
|
ec9db7f9a2 | ||
|
|
0d796a0fb9 | ||
|
|
96735e41af | ||
|
|
fef1dc8c54 | ||
|
|
ef5ca63bf0 | ||
|
|
d218ba98e7 | ||
|
|
d53a00d054 | ||
|
|
62fcb1895e | ||
|
|
843bad83fa | ||
|
|
e850ccb82c | ||
|
|
f4e7364651 | ||
|
|
82ff5cbe0f | ||
|
|
e11e6e1b04 | ||
|
|
2863ac1068 | ||
|
|
3d04856cbd | ||
|
|
7c55b9f087 | ||
|
|
6681605c34 | ||
|
|
95bbea20a8 | ||
|
|
662375bdd7 | ||
|
|
aa26f90420 | ||
|
|
c61b6cf616 | ||
|
|
16d8e92b06 | ||
|
|
68d3e624e6 | ||
|
|
d505f1c5f2 | ||
|
|
b252d8e2cd | ||
|
|
c040f7abc0 | ||
|
|
2871a650f6 | ||
|
|
5b0ee473b6 | ||
|
|
00d26b3049 | ||
|
|
ddb5ff3b71 | ||
|
|
72bbe2203e | ||
|
|
2a720efbd4 | ||
|
|
baeb3cddc6 | ||
|
|
ee88433fb1 | ||
|
|
845d81bdae | ||
|
|
d0f9595ad9 | ||
|
|
9007e17c3e | ||
|
|
e85af58e43 | ||
|
|
0c90bfb936 | ||
|
|
8daba68dc1 | ||
|
|
e3981b6498 | ||
|
|
a89c7f8feb | ||
|
|
357631d659 | ||
|
|
3b0660ae89 | ||
|
|
a8632480ff | ||
|
|
80653824d9 | ||
|
|
b3c7142030 | ||
|
|
df32830f17 | ||
|
|
b697bb7a26 | ||
|
|
a3ecde01ee | ||
|
|
e2ed2ecdc0 | ||
|
|
1e0bc97f56 | ||
|
|
eebb452fb5 | ||
|
|
2c42e1a5cb | ||
|
|
28c411c742 | ||
|
|
9d8d8afa82 | ||
|
|
31e514ec15 | ||
|
|
73a7d5e6f4 | ||
|
|
f584878204 | ||
|
|
0533f56fe3 | ||
|
|
416af5cf57 | ||
|
|
557211240e | ||
|
|
13e0691c90 | ||
|
|
0e429cca33 | ||
|
|
887e1cd8e3 | ||
|
|
c899e2a662 | ||
|
|
e7054e0fd2 | ||
|
|
9cf9be8850 | ||
|
|
b1b269b302 | ||
|
|
6e300bd438 | ||
|
|
21a194f9d8 | ||
|
|
b3a8b0056b | ||
|
|
b2a7699cdf | ||
|
|
3e443d253c | ||
|
|
6a7bd19a5a | ||
|
|
dbe0ba87a3 | ||
|
|
bea7e2a7fa | ||
|
|
eac2388d49 | ||
|
|
7a84cfb0be | ||
|
|
b0ce3dc683 | ||
|
|
70ba5eb0ef | ||
|
|
1761b25879 | ||
|
|
c2b4e24372 | ||
|
|
5e363d124e | ||
|
|
a52f96b23a | ||
|
|
620c6a22ac | ||
|
|
e1d1f21a74 | ||
|
|
66b2ed930c | ||
|
|
33b8241d26 | ||
|
|
37cd711c96 | ||
|
|
0eb8c77889 | ||
|
|
4be30f7c88 | ||
|
|
70c5bd4316 | ||
|
|
daf2f30822 | ||
|
|
fda483f482 | ||
|
|
c2cce13e2a | ||
|
|
38d23ba0af | ||
|
|
5e1338a9e4 | ||
|
|
4ac9a2e9de | ||
|
|
f57191e8dd | ||
|
|
11fb4866a8 | ||
|
|
d9fb3c8c28 | ||
|
|
df475cb797 | ||
|
|
f588fef3b4 | ||
|
|
6e4083d7f4 | ||
|
|
4a2a130bfa | ||
|
|
ce8ec3acb1 | ||
|
|
474ac8b09e | ||
|
|
77244eab1e | ||
|
|
6bb4199824 | ||
|
|
f6349a6cf4 | ||
|
|
fa73b8e37a | ||
|
|
0afa01609c | ||
|
|
723d00d33a |
@@ -57,7 +57,7 @@ commands:
|
||||
<<# 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
|
||||
no_output_timeout: 15m
|
||||
- save_cache:
|
||||
paths:
|
||||
- ./venv
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
name: run static check
|
||||
command: |
|
||||
. venv/bin/activate
|
||||
flake8
|
||||
flake8 homeassistant tests script
|
||||
|
||||
- run:
|
||||
name: run static type check
|
||||
|
||||
@@ -13,3 +13,4 @@ coverage:
|
||||
url: "secret:TgWDUM4Jw0w7wMJxuxNF/yhSOHglIo1fGwInJnRLEVPy2P2aLimkoK1mtKCowH5TFw+baUXVXT3eAqefbdvIuM8BjRR4aRji95C6CYyD0QHy4N8i7nn1SQkWDPpS8IthYTg07rUDF7s5guurkKv2RrgoCdnnqjAMSzHoExMOF7xUmblMdhBTWJgBpWEhASJy85w/xxjlsE1xoTkzeJu9Q67pTXtRcn+5kb5/vIzPSYg="
|
||||
comment:
|
||||
require_changes: yes
|
||||
branches: master
|
||||
|
||||
24
.coveragerc
24
.coveragerc
@@ -22,6 +22,7 @@ omit =
|
||||
homeassistant/components/alarmdotcom/alarm_control_panel.py
|
||||
homeassistant/components/alpha_vantage/sensor.py
|
||||
homeassistant/components/amazon_polly/tts.py
|
||||
homeassistant/components/ambiclimate/climate.py
|
||||
homeassistant/components/ambient_station/*
|
||||
homeassistant/components/amcrest/*
|
||||
homeassistant/components/ampio/*
|
||||
@@ -46,12 +47,14 @@ omit =
|
||||
homeassistant/components/august/*
|
||||
homeassistant/components/automatic/device_tracker.py
|
||||
homeassistant/components/avion/light.py
|
||||
homeassistant/components/azure_event_hub/*
|
||||
homeassistant/components/baidu/tts.py
|
||||
homeassistant/components/bbb_gpio/*
|
||||
homeassistant/components/bbox/device_tracker.py
|
||||
homeassistant/components/bbox/sensor.py
|
||||
homeassistant/components/bh1750/sensor.py
|
||||
homeassistant/components/bitcoin/sensor.py
|
||||
homeassistant/components/bizkaibus/sensor.py
|
||||
homeassistant/components/blink/*
|
||||
homeassistant/components/blinksticklight/light.py
|
||||
homeassistant/components/blinkt/light.py
|
||||
@@ -169,10 +172,12 @@ omit =
|
||||
homeassistant/components/esphome/camera.py
|
||||
homeassistant/components/esphome/climate.py
|
||||
homeassistant/components/esphome/cover.py
|
||||
homeassistant/components/esphome/entry_data.py
|
||||
homeassistant/components/esphome/fan.py
|
||||
homeassistant/components/esphome/light.py
|
||||
homeassistant/components/esphome/sensor.py
|
||||
homeassistant/components/esphome/switch.py
|
||||
homeassistant/components/essent/sensor.py
|
||||
homeassistant/components/etherscan/sensor.py
|
||||
homeassistant/components/eufy/*
|
||||
homeassistant/components/everlights/light.py
|
||||
@@ -247,7 +252,6 @@ omit =
|
||||
homeassistant/components/hitron_coda/device_tracker.py
|
||||
homeassistant/components/hive/*
|
||||
homeassistant/components/hlk_sw16/*
|
||||
homeassistant/components/homekit_controller/*
|
||||
homeassistant/components/homematic/*
|
||||
homeassistant/components/homematic/climate.py
|
||||
homeassistant/components/homematic/cover.py
|
||||
@@ -275,9 +279,11 @@ omit =
|
||||
homeassistant/components/imap_email_content/sensor.py
|
||||
homeassistant/components/influxdb/sensor.py
|
||||
homeassistant/components/insteon/*
|
||||
homeassistant/components/incomfort/*
|
||||
homeassistant/components/ios/*
|
||||
homeassistant/components/iota/*
|
||||
homeassistant/components/iperf3/*
|
||||
homeassistant/components/iqvia/*
|
||||
homeassistant/components/irish_rail_transport/sensor.py
|
||||
homeassistant/components/iss/binary_sensor.py
|
||||
homeassistant/components/isy994/*
|
||||
@@ -339,11 +345,13 @@ omit =
|
||||
homeassistant/components/mastodon/notify.py
|
||||
homeassistant/components/matrix/*
|
||||
homeassistant/components/maxcube/*
|
||||
homeassistant/components/mcp23017/*
|
||||
homeassistant/components/media_extractor/*
|
||||
homeassistant/components/mediaroom/media_player.py
|
||||
homeassistant/components/message_bird/notify.py
|
||||
homeassistant/components/met/weather.py
|
||||
homeassistant/components/meteo_france/*
|
||||
homeassistant/components/meteoalarm/*
|
||||
homeassistant/components/metoffice/sensor.py
|
||||
homeassistant/components/metoffice/weather.py
|
||||
homeassistant/components/microsoft/tts.py
|
||||
@@ -418,6 +426,7 @@ omit =
|
||||
homeassistant/components/openweathermap/sensor.py
|
||||
homeassistant/components/openweathermap/weather.py
|
||||
homeassistant/components/opple/light.py
|
||||
homeassistant/components/orangepi_gpio/*
|
||||
homeassistant/components/orvibo/switch.py
|
||||
homeassistant/components/osramlightify/light.py
|
||||
homeassistant/components/otp/sensor.py
|
||||
@@ -440,7 +449,6 @@ omit =
|
||||
homeassistant/components/plum_lightpad/*
|
||||
homeassistant/components/pocketcasts/sensor.py
|
||||
homeassistant/components/point/*
|
||||
homeassistant/components/pollen/sensor.py
|
||||
homeassistant/components/postnl/sensor.py
|
||||
homeassistant/components/prezzibenzina/sensor.py
|
||||
homeassistant/components/proliphix/climate.py
|
||||
@@ -449,6 +457,7 @@ omit =
|
||||
homeassistant/components/proxy/camera.py
|
||||
homeassistant/components/ps4/__init__.py
|
||||
homeassistant/components/ps4/media_player.py
|
||||
homeassistant/components/ptvsd/*
|
||||
homeassistant/components/pulseaudio_loopback/switch.py
|
||||
homeassistant/components/pushbullet/notify.py
|
||||
homeassistant/components/pushbullet/sensor.py
|
||||
@@ -480,6 +489,9 @@ omit =
|
||||
homeassistant/components/reddit/*
|
||||
homeassistant/components/rejseplanen/sensor.py
|
||||
homeassistant/components/remember_the_milk/__init__.py
|
||||
homeassistant/components/repetier/__init__.py
|
||||
homeassistant/components/repetier/sensor.py
|
||||
homeassistant/components/remote_rpi_gpio/*
|
||||
homeassistant/components/rest/binary_sensor.py
|
||||
homeassistant/components/rest/notify.py
|
||||
homeassistant/components/rest/switch.py
|
||||
@@ -532,14 +544,14 @@ omit =
|
||||
homeassistant/components/slack/notify.py
|
||||
homeassistant/components/sma/sensor.py
|
||||
homeassistant/components/smappee/*
|
||||
homeassistant/components/smarthab/*
|
||||
homeassistant/components/smtp/notify.py
|
||||
homeassistant/components/snapcast/media_player.py
|
||||
homeassistant/components/snmp/device_tracker.py
|
||||
homeassistant/components/snmp/sensor.py
|
||||
homeassistant/components/snmp/switch.py
|
||||
homeassistant/components/snmp/*
|
||||
homeassistant/components/sochain/sensor.py
|
||||
homeassistant/components/socialblade/sensor.py
|
||||
homeassistant/components/solaredge/sensor.py
|
||||
homeassistant/components/solax/sensor.py
|
||||
homeassistant/components/somfy_mylink/*
|
||||
homeassistant/components/sonarr/sensor.py
|
||||
homeassistant/components/songpal/media_player.py
|
||||
@@ -561,6 +573,7 @@ omit =
|
||||
homeassistant/components/swiss_public_transport/sensor.py
|
||||
homeassistant/components/swisscom/device_tracker.py
|
||||
homeassistant/components/switchbot/switch.py
|
||||
homeassistant/components/switcher_kis/switch.py
|
||||
homeassistant/components/switchmate/switch.py
|
||||
homeassistant/components/syncthru/sensor.py
|
||||
homeassistant/components/synology/camera.py
|
||||
@@ -645,6 +658,7 @@ omit =
|
||||
homeassistant/components/waqi/sensor.py
|
||||
homeassistant/components/waterfurnace/*
|
||||
homeassistant/components/watson_iot/*
|
||||
homeassistant/components/watson_tts/tts.py
|
||||
homeassistant/components/waze_travel_time/sensor.py
|
||||
homeassistant/components/webostv/*
|
||||
homeassistant/components/wemo/*
|
||||
|
||||
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -7,7 +7,7 @@
|
||||
|
||||
**Related issue (if applicable):** fixes #<home-assistant issue number goes here>
|
||||
|
||||
**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io#<home-assistant.io PR number goes here>
|
||||
**Pull request with documentation for [home-assistant.io](https://github.com/home-assistant/home-assistant.io) (if applicable):** home-assistant/home-assistant.io#<home-assistant.io PR number goes here>
|
||||
|
||||
## Example entry for `configuration.yaml` (if applicable):
|
||||
```yaml
|
||||
@@ -18,21 +18,18 @@
|
||||
- [ ] The code change is tested and works locally.
|
||||
- [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
|
||||
- [ ] There is no commented out code in this PR.
|
||||
- [ ] I have followed the [development checklist][dev-checklist]
|
||||
|
||||
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:
|
||||
- [ ] [_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`.
|
||||
- [ ] [_The manifest file_][manifest-docs] has all fields filled out correctly. Update and include derived files by running `python3 -m script.hassfest`.
|
||||
- [ ] New or updated dependencies have been added to `requirements_all.txt` by running `python3 -m script.gen_requirements_all`.
|
||||
- [ ] Untested files have been added to `.coveragerc`.
|
||||
|
||||
If the code does not interact with devices:
|
||||
- [ ] Tests have been added to verify that the new code works.
|
||||
|
||||
[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_
|
||||
[dev-checklist]: https://developers.home-assistant.io/docs/en/development_checklist.html
|
||||
[manifest-docs]: https://developers.home-assistant.io/docs/en/creating_integration_manifest.html
|
||||
|
||||
14
.github/main.workflow
vendored
14
.github/main.workflow
vendored
@@ -1,14 +0,0 @@
|
||||
workflow "Mention CODEOWNERS of integrations when integration label is added to an issue" {
|
||||
on = "issues"
|
||||
resolves = "codeowners-mention"
|
||||
}
|
||||
|
||||
workflow "Mention CODEOWNERS of integrations when integration label is added to an PRs" {
|
||||
on = "pull_request"
|
||||
resolves = "codeowners-mention"
|
||||
}
|
||||
|
||||
action "codeowners-mention" {
|
||||
uses = "home-assistant/codeowners-mention@master"
|
||||
secrets = ["GITHUB_TOKEN"]
|
||||
}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
config/*
|
||||
config2/*
|
||||
|
||||
tests/testing_config/deps
|
||||
tests/testing_config/home-assistant.log
|
||||
@@ -84,7 +85,7 @@ Scripts/
|
||||
# vimmy stuff
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
tags
|
||||
ctags.tmp
|
||||
|
||||
# vagrant stuff
|
||||
|
||||
33
.travis.yml
Normal file
33
.travis.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
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=py35
|
||||
- python: "3.7"
|
||||
env: TOXENV=py37
|
||||
|
||||
cache: pip
|
||||
install: pip install -U tox
|
||||
language: python
|
||||
script: travis_wait 40 tox --develop
|
||||
44
CODEOWNERS
44
CODEOWNERS
@@ -21,6 +21,7 @@ homeassistant/components/airvisual/* @bachya
|
||||
homeassistant/components/alarm_control_panel/* @colinodell
|
||||
homeassistant/components/alpha_vantage/* @fabaff
|
||||
homeassistant/components/amazon_polly/* @robbiet480
|
||||
homeassistant/components/ambiclimate/* @danielhiversen
|
||||
homeassistant/components/ambient_station/* @bachya
|
||||
homeassistant/components/api/* @home-assistant/core
|
||||
homeassistant/components/arduino/* @fabaff
|
||||
@@ -31,7 +32,9 @@ homeassistant/components/automatic/* @armills
|
||||
homeassistant/components/automation/* @home-assistant/core
|
||||
homeassistant/components/aws/* @awarecan @robbiet480
|
||||
homeassistant/components/axis/* @kane610
|
||||
homeassistant/components/azure_event_hub/* @eavanvalkenburg
|
||||
homeassistant/components/bitcoin/* @fabaff
|
||||
homeassistant/components/bizkaibus/* @UgaitzEtxebarria
|
||||
homeassistant/components/blink/* @fronzbot
|
||||
homeassistant/components/bmw_connected_drive/* @ChristianKuehnel
|
||||
homeassistant/components/braviatv/* @robbiet480
|
||||
@@ -66,10 +69,13 @@ homeassistant/components/egardia/* @jeroenterheerdt
|
||||
homeassistant/components/eight_sleep/* @mezz64
|
||||
homeassistant/components/emby/* @mezz64
|
||||
homeassistant/components/enigma2/* @fbradyirl
|
||||
homeassistant/components/enocean/* @bdurrer
|
||||
homeassistant/components/ephember/* @ttroy50
|
||||
homeassistant/components/epsonworkforce/* @ThaStealth
|
||||
homeassistant/components/eq3btsmart/* @rytilahti
|
||||
homeassistant/components/esphome/* @OttoWinter
|
||||
homeassistant/components/essent/* @TheLastProject
|
||||
homeassistant/components/evohome/* @zxdavb
|
||||
homeassistant/components/file/* @fabaff
|
||||
homeassistant/components/filter/* @dgomes
|
||||
homeassistant/components/fitbit/* @robbiet480
|
||||
@@ -78,8 +84,9 @@ homeassistant/components/flock/* @fabaff
|
||||
homeassistant/components/flunearyou/* @bachya
|
||||
homeassistant/components/foursquare/* @robbiet480
|
||||
homeassistant/components/freebox/* @snoof85
|
||||
homeassistant/components/frontend/* @home-assistant/core
|
||||
homeassistant/components/frontend/* @home-assistant/frontend
|
||||
homeassistant/components/gearbest/* @HerrHofrat
|
||||
homeassistant/components/geniushub/* @zxdavb
|
||||
homeassistant/components/gitter/* @fabaff
|
||||
homeassistant/components/glances/* @fabaff
|
||||
homeassistant/components/gntp/* @robbiet480
|
||||
@@ -99,6 +106,7 @@ homeassistant/components/history_graph/* @andrey-git
|
||||
homeassistant/components/hive/* @Rendili @KJonline
|
||||
homeassistant/components/homeassistant/* @home-assistant/core
|
||||
homeassistant/components/homekit/* @cdce8p
|
||||
homeassistant/components/homekit_controller/* @Jc2k
|
||||
homeassistant/components/homematic/* @pvizeli @danielperna84
|
||||
homeassistant/components/html5/* @robbiet480
|
||||
homeassistant/components/http/* @home-assistant/core
|
||||
@@ -106,6 +114,7 @@ homeassistant/components/huawei_lte/* @scop
|
||||
homeassistant/components/huawei_router/* @abmantis
|
||||
homeassistant/components/hue/* @balloob
|
||||
homeassistant/components/ign_sismologia/* @exxamalte
|
||||
homeassistant/components/incomfort/* @zxdavb
|
||||
homeassistant/components/influxdb/* @fabaff
|
||||
homeassistant/components/input_boolean/* @home-assistant/core
|
||||
homeassistant/components/input_datetime/* @home-assistant/core
|
||||
@@ -115,6 +124,7 @@ homeassistant/components/input_text/* @home-assistant/core
|
||||
homeassistant/components/integration/* @dgomes
|
||||
homeassistant/components/ios/* @robbiet480
|
||||
homeassistant/components/ipma/* @dgomes
|
||||
homeassistant/components/iqvia/* @bachya
|
||||
homeassistant/components/irish_rail_transport/* @ttroy50
|
||||
homeassistant/components/jewish_calendar/* @tsvi
|
||||
homeassistant/components/knx/* @Julius2342
|
||||
@@ -122,6 +132,7 @@ homeassistant/components/kodi/* @armills
|
||||
homeassistant/components/konnected/* @heythisisnate
|
||||
homeassistant/components/lametric/* @robbiet480
|
||||
homeassistant/components/launch_library/* @ludeeus
|
||||
homeassistant/components/lcn/* @alengwenus
|
||||
homeassistant/components/lifx/* @amelchio
|
||||
homeassistant/components/lifx_cloud/* @amelchio
|
||||
homeassistant/components/lifx_legacy/* @amelchio
|
||||
@@ -129,14 +140,16 @@ 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/lovelace/* @home-assistant/frontend
|
||||
homeassistant/components/luci/* @fbradyirl
|
||||
homeassistant/components/luftdaten/* @fabaff
|
||||
homeassistant/components/mastodon/* @fabaff
|
||||
homeassistant/components/matrix/* @tinloaf
|
||||
homeassistant/components/mcp23017/* @jardiamj
|
||||
homeassistant/components/mediaroom/* @dgomes
|
||||
homeassistant/components/melissa/* @kennedyshead
|
||||
homeassistant/components/met/* @danielhiversen
|
||||
homeassistant/components/meteoalarm/* @rolfberkenbosch
|
||||
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/mill/* @danielhiversen
|
||||
homeassistant/components/min_max/* @fabaff
|
||||
@@ -150,24 +163,28 @@ homeassistant/components/nello/* @pschmitt
|
||||
homeassistant/components/ness_alarm/* @nickw444
|
||||
homeassistant/components/nest/* @awarecan
|
||||
homeassistant/components/netdata/* @fabaff
|
||||
homeassistant/components/nextbus/* @vividboarder
|
||||
homeassistant/components/nissan_leaf/* @filcole
|
||||
homeassistant/components/nmbs/* @thibmaek
|
||||
homeassistant/components/no_ip/* @fabaff
|
||||
homeassistant/components/notify/* @flowolf
|
||||
homeassistant/components/notify/* @home-assistant/core
|
||||
homeassistant/components/nsw_fuel_station/* @nickw444
|
||||
homeassistant/components/nuki/* @pschmitt
|
||||
homeassistant/components/ohmconnect/* @robbiet480
|
||||
homeassistant/components/onboarding/* @home-assistant/core
|
||||
homeassistant/components/openuv/* @bachya
|
||||
homeassistant/components/openweathermap/* @fabaff
|
||||
homeassistant/components/orangepi_gpio/* @pascallj
|
||||
homeassistant/components/owlet/* @oblogic7
|
||||
homeassistant/components/panel_custom/* @home-assistant/core
|
||||
homeassistant/components/panel_iframe/* @home-assistant/core
|
||||
homeassistant/components/panel_custom/* @home-assistant/frontend
|
||||
homeassistant/components/panel_iframe/* @home-assistant/frontend
|
||||
homeassistant/components/persistent_notification/* @home-assistant/core
|
||||
homeassistant/components/philips_js/* @elupus
|
||||
homeassistant/components/pi_hole/* @fabaff
|
||||
homeassistant/components/plant/* @ChristianKuehnel
|
||||
homeassistant/components/point/* @fredrike
|
||||
homeassistant/components/pollen/* @bachya
|
||||
homeassistant/components/ps4/* @ktnrg45
|
||||
homeassistant/components/ptvsd/* @swamp-ig
|
||||
homeassistant/components/push/* @dgomes
|
||||
homeassistant/components/pvoutput/* @fabaff
|
||||
homeassistant/components/qnap/* @colinodell
|
||||
@@ -176,6 +193,7 @@ homeassistant/components/qwikswitch/* @kellerza
|
||||
homeassistant/components/raincloud/* @vanstinator
|
||||
homeassistant/components/rainmachine/* @bachya
|
||||
homeassistant/components/random/* @fabaff
|
||||
homeassistant/components/repetier/* @MTrab
|
||||
homeassistant/components/rfxtrx/* @danielhiversen
|
||||
homeassistant/components/rmvtransport/* @cgtobi
|
||||
homeassistant/components/roomba/* @pschmitt
|
||||
@@ -191,20 +209,24 @@ homeassistant/components/shiftr/* @fabaff
|
||||
homeassistant/components/shodan/* @fabaff
|
||||
homeassistant/components/simplisafe/* @bachya
|
||||
homeassistant/components/sma/* @kellerza
|
||||
homeassistant/components/smarthab/* @outadoc
|
||||
homeassistant/components/smartthings/* @andrewsayre
|
||||
homeassistant/components/smtp/* @fabaff
|
||||
homeassistant/components/solax/* @squishykid
|
||||
homeassistant/components/sonos/* @amelchio
|
||||
homeassistant/components/spaceapi/* @fabaff
|
||||
homeassistant/components/spider/* @peternijssen
|
||||
homeassistant/components/sql/* @dgomes
|
||||
homeassistant/components/statistics/* @fabaff
|
||||
homeassistant/components/stiebel_eltron/* @fucm
|
||||
homeassistant/components/sun/* @home-assistant/core
|
||||
homeassistant/components/sun/* @Swamp-Ig
|
||||
homeassistant/components/supla/* @mwegrzynek
|
||||
homeassistant/components/swiss_hydrological_data/* @fabaff
|
||||
homeassistant/components/swiss_public_transport/* @fabaff
|
||||
homeassistant/components/switchbot/* @danielhiversen
|
||||
homeassistant/components/switcher_kis/* @tomerfi
|
||||
homeassistant/components/switchmate/* @danielhiversen
|
||||
homeassistant/components/syncthru/* @nielstron
|
||||
homeassistant/components/synology_srm/* @aerialls
|
||||
homeassistant/components/syslog/* @fabaff
|
||||
homeassistant/components/sytadin/* @gautric
|
||||
@@ -235,7 +257,9 @@ homeassistant/components/uptimerobot/* @ludeeus
|
||||
homeassistant/components/utility_meter/* @dgomes
|
||||
homeassistant/components/velux/* @Julius2342
|
||||
homeassistant/components/version/* @fabaff
|
||||
homeassistant/components/vizio/* @raman325
|
||||
homeassistant/components/waqi/* @andrey-git
|
||||
homeassistant/components/watson_tts/* @rutkai
|
||||
homeassistant/components/weather/* @fabaff
|
||||
homeassistant/components/weblink/* @home-assistant/core
|
||||
homeassistant/components/websocket_api/* @home-assistant/core
|
||||
@@ -244,14 +268,14 @@ homeassistant/components/worldclock/* @fabaff
|
||||
homeassistant/components/xfinity/* @cisasteelersfan
|
||||
homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
|
||||
homeassistant/components/xiaomi_miio/* @rytilahti @syssi
|
||||
homeassistant/components/xiaomi_tv/* @fattdev
|
||||
homeassistant/components/xmpp/* @fabaff
|
||||
homeassistant/components/xiaomi_tv/* @simse
|
||||
homeassistant/components/xmpp/* @fabaff @flowolf
|
||||
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/zeroconf/* @robbiet480 @Kane610
|
||||
homeassistant/components/zha/* @dmulcahey @adminiuga
|
||||
homeassistant/components/zone/* @home-assistant/core
|
||||
homeassistant/components/zoneminder/* @rohankapoorcom
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Home Assistant |Build Status| |CI Status| |Coverage Status| |Chat Status|
|
||||
Home Assistant |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,12 +27,6 @@ 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=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
|
||||
:target: https://discord.gg/c5DvZ4e
|
||||
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
|
||||
|
||||
210
azure-pipelines.yml
Normal file
210
azure-pipelines.yml
Normal file
@@ -0,0 +1,210 @@
|
||||
# https://dev.azure.com/home-assistant
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- dev
|
||||
tags:
|
||||
include:
|
||||
- '*'
|
||||
variables:
|
||||
- name: versionBuilder
|
||||
value: '3.2'
|
||||
- name: versionWheels
|
||||
value: '0.3'
|
||||
- group: docker
|
||||
- group: wheels
|
||||
- group: github
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
- job: 'Wheels'
|
||||
condition: eq(variables['Build.SourceBranchName'], 'dev')
|
||||
timeoutInMinutes: 360
|
||||
pool:
|
||||
vmImage: 'ubuntu-16.04'
|
||||
strategy:
|
||||
maxParallel: 3
|
||||
matrix:
|
||||
amd64:
|
||||
buildArch: 'amd64'
|
||||
i386:
|
||||
buildArch: 'i386'
|
||||
armhf:
|
||||
buildArch: 'armhf'
|
||||
armv7:
|
||||
buildArch: 'armv7'
|
||||
aarch64:
|
||||
buildArch: 'aarch64'
|
||||
steps:
|
||||
- script: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
qemu-user-static \
|
||||
binfmt-support
|
||||
|
||||
sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
|
||||
sudo update-binfmts --enable qemu-arm
|
||||
sudo update-binfmts --enable qemu-aarch64
|
||||
displayName: 'Initial cross build'
|
||||
- script: |
|
||||
mkdir -p .ssh
|
||||
echo -e "-----BEGIN RSA PRIVATE KEY-----\n$(wheelsSSH)\n-----END RSA PRIVATE KEY-----" >> .ssh/id_rsa
|
||||
ssh-keyscan -H $(wheelsHost) >> .ssh/known_hosts
|
||||
chmod 600 .ssh/*
|
||||
displayName: 'Install ssh key'
|
||||
- script: sudo docker pull homeassistant/$(buildArch)-wheels:$(versionWheels)
|
||||
displayName: 'Install wheels builder'
|
||||
- script: |
|
||||
cp requirements_all.txt requirements_hassio.txt
|
||||
|
||||
# Enable because we can build it
|
||||
sed -i "s|# pytradfri|pytradfri|g" requirements_hassio.txt
|
||||
sed -i "s|# pybluez|pybluez|g" requirements_hassio.txt
|
||||
sed -i "s|# bluepy|bluepy|g" requirements_hassio.txt
|
||||
sed -i "s|# beacontools|beacontools|g" requirements_hassio.txt
|
||||
sed -i "s|# RPi.GPIO|RPi.GPIO|g" requirements_hassio.txt
|
||||
sed -i "s|# raspihats|raspihats|g" requirements_hassio.txt
|
||||
sed -i "s|# rpi-rf|rpi-rf|g" requirements_hassio.txt
|
||||
sed -i "s|# blinkt|blinkt|g" requirements_hassio.txt
|
||||
sed -i "s|# fritzconnection|fritzconnection|g" requirements_hassio.txt
|
||||
sed -i "s|# pyuserinput|pyuserinput|g" requirements_hassio.txt
|
||||
sed -i "s|# evdev|evdev|g" requirements_hassio.txt
|
||||
sed -i "s|# smbus-cffi|smbus-cffi|g" requirements_hassio.txt
|
||||
sed -i "s|# i2csense|i2csense|g" requirements_hassio.txt
|
||||
sed -i "s|# python-eq3bt|python-eq3bt|g" requirements_hassio.txt
|
||||
sed -i "s|# pycups|pycups|g" requirements_hassio.txt
|
||||
sed -i "s|# homekit|homekit|g" requirements_hassio.txt
|
||||
sed -i "s|# decora_wifi|decora_wifi|g" requirements_hassio.txt
|
||||
sed -i "s|# decora|decora|g" requirements_hassio.txt
|
||||
sed -i "s|# PySwitchbot|PySwitchbot|g" requirements_hassio.txt
|
||||
sed -i "s|# pySwitchmate|pySwitchmate|g" requirements_hassio.txt
|
||||
|
||||
# Disable because of error
|
||||
sed -i "s|insteonplm|# insteonplm|g" requirements_hassio.txt
|
||||
displayName: 'Prepare requirements files for Hass.io'
|
||||
- script: |
|
||||
sudo docker run --rm -v $(pwd):/data:ro -v $(pwd)/.ssh:/root/.ssh:rw \
|
||||
homeassistant/$(buildArch)-wheels:$(versionWheels) \
|
||||
--apk "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;linux-headers;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev" \
|
||||
--index https://wheels.hass.io \
|
||||
--requirement requirements_hassio.txt \
|
||||
--upload rsync \
|
||||
--remote wheels@$(wheelsHost):/opt/wheels
|
||||
displayName: 'Run wheels build'
|
||||
|
||||
|
||||
- job: 'VersionValidate'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
displayName: 'Use Python 3.7'
|
||||
inputs:
|
||||
versionSpec: '3.7'
|
||||
- script: |
|
||||
setup_version="$(python setup.py -V)"
|
||||
branch_version="$(Build.SourceBranchName)"
|
||||
|
||||
if [ "${setup_version}" != "${branch_version}" ]; then
|
||||
echo "Version of tag ${branch_version} don't match with ${setup_version}!"
|
||||
exit 1
|
||||
fi
|
||||
displayName: 'Check version of branch/tag'
|
||||
|
||||
|
||||
- job: 'Release'
|
||||
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate'))
|
||||
dependsOn:
|
||||
- 'VersionValidate'
|
||||
timeoutInMinutes: 120
|
||||
pool:
|
||||
vmImage: 'ubuntu-16.04'
|
||||
strategy:
|
||||
maxParallel: 5
|
||||
matrix:
|
||||
amd64:
|
||||
buildArch: 'amd64'
|
||||
buildMachine: 'qemux86-64,intel-nuc'
|
||||
i386:
|
||||
buildArch: 'i386'
|
||||
buildMachine: 'qemux86'
|
||||
armhf:
|
||||
buildArch: 'armhf'
|
||||
buildMachine: 'qemuarm,raspberrypi'
|
||||
armv7:
|
||||
buildArch: 'armv7'
|
||||
buildMachine: 'raspberrypi2,raspberrypi3,odroid-xu,tinker'
|
||||
aarch64:
|
||||
buildArch: 'aarch64'
|
||||
buildMachine: 'qemuarm-64,raspberrypi3-64,odroid-c2,orangepi-prime'
|
||||
steps:
|
||||
- script: sudo docker login -u $(dockerUser) -p $(dockerPassword)
|
||||
displayName: 'Docker hub login'
|
||||
- script: sudo docker pull homeassistant/amd64-builder:$(versionBuilder)
|
||||
displayName: 'Install Builder'
|
||||
- script: |
|
||||
set -e
|
||||
|
||||
sudo docker run --rm --privileged \
|
||||
-v ~/.docker:/root/.docker \
|
||||
-v /run/docker.sock:/run/docker.sock:rw \
|
||||
homeassistant/amd64-builder:$(versionBuilder) \
|
||||
--homeassistant $(Build.SourceBranchName) "--$(buildArch)" \
|
||||
-r https://github.com/home-assistant/hassio-homeassistant \
|
||||
-t generic --docker-hub homeassistant
|
||||
|
||||
sudo docker run --rm --privileged \
|
||||
-v ~/.docker:/root/.docker \
|
||||
-v /run/docker.sock:/run/docker.sock:rw \
|
||||
homeassistant/amd64-builder:$(versionBuilder) \
|
||||
--homeassistant-machine "$(Build.SourceBranchName)=$(buildMachine)" \
|
||||
-r https://github.com/home-assistant/hassio-homeassistant \
|
||||
-t machine --docker-hub homeassistant
|
||||
displayName: 'Build Release'
|
||||
|
||||
|
||||
- job: 'ReleasePublish'
|
||||
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('Release'))
|
||||
dependsOn:
|
||||
- 'Release'
|
||||
pool:
|
||||
vmImage: 'ubuntu-16.04'
|
||||
steps:
|
||||
- script: |
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
git jq
|
||||
|
||||
git config --global user.name "Pascal Vizeli"
|
||||
git config --global user.email "pvizeli@syshack.ch"
|
||||
git config --global credential.helper store
|
||||
|
||||
echo "https://$(githubToken):x-oauth-basic@github.com" > $HOME/.git-credentials
|
||||
displayName: 'Install requirements'
|
||||
- script: |
|
||||
set -e
|
||||
|
||||
version="$(Build.SourceBranchName)"
|
||||
|
||||
git clone https://github.com/home-assistant/hassio-version
|
||||
cd hassio-version
|
||||
|
||||
dev_version="$(jq --raw-output '.homeassistant.default' dev.json)"
|
||||
beta_version="$(jq --raw-output '.homeassistant.default' beta.json)"
|
||||
stable_version="$(jq --raw-output '.homeassistant.default' stable.json)"
|
||||
|
||||
if [[ "$version" =~ b ]]; then
|
||||
sed -i "s|$dev_version|$version|g" dev.json
|
||||
sed -i "s|$beta_version|$version|g" beta.json
|
||||
else
|
||||
sed -i "s|$dev_version|$version|g" dev.json
|
||||
sed -i "s|$beta_version|$version|g" beta.json
|
||||
sed -i "s|$stable_version|$version|g" stable.json
|
||||
fi
|
||||
|
||||
git commit -am "Bump Home Assistant $version"
|
||||
git push
|
||||
displayName: 'Update version files'
|
||||
@@ -7,8 +7,9 @@ import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from typing import List, Dict, Any # noqa pylint: disable=unused-import
|
||||
|
||||
from typing import ( # noqa pylint: disable=unused-import
|
||||
List, Dict, Any, TYPE_CHECKING
|
||||
)
|
||||
|
||||
from homeassistant import monkey_patch
|
||||
from homeassistant.const import (
|
||||
@@ -18,6 +19,9 @@ from homeassistant.const import (
|
||||
RESTART_EXIT_CODE,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant import core
|
||||
|
||||
|
||||
def set_loop() -> None:
|
||||
"""Attempt to use uvloop."""
|
||||
@@ -86,10 +90,12 @@ def ensure_config_path(config_dir: str) -> None:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def ensure_config_file(config_dir: str) -> str:
|
||||
async def ensure_config_file(hass: 'core.HomeAssistant', config_dir: str) \
|
||||
-> str:
|
||||
"""Ensure configuration file exists."""
|
||||
import homeassistant.config as config_util
|
||||
config_path = config_util.ensure_config_exists(config_dir)
|
||||
config_path = await config_util.async_ensure_config_exists(
|
||||
hass, config_dir)
|
||||
|
||||
if config_path is None:
|
||||
print('Error getting configuration path')
|
||||
@@ -261,6 +267,7 @@ def cmdline() -> List[str]:
|
||||
async def setup_and_run_hass(config_dir: str,
|
||||
args: argparse.Namespace) -> int:
|
||||
"""Set up HASS and run."""
|
||||
# pylint: disable=redefined-outer-name
|
||||
from homeassistant import bootstrap, core
|
||||
|
||||
hass = core.HomeAssistant()
|
||||
@@ -275,7 +282,7 @@ async def setup_and_run_hass(config_dir: str,
|
||||
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
|
||||
log_file=args.log_file, log_no_color=args.log_no_color)
|
||||
else:
|
||||
config_file = ensure_config_file(config_dir)
|
||||
config_file = await ensure_config_file(hass, config_dir)
|
||||
print('Config directory:', config_dir)
|
||||
await bootstrap.async_from_config_file(
|
||||
config_file, hass, verbose=args.verbose, skip_pip=args.skip_pip,
|
||||
@@ -390,7 +397,7 @@ def main() -> int:
|
||||
if exit_code == RESTART_EXIT_CODE and not args.runner:
|
||||
try_to_restart()
|
||||
|
||||
return exit_code # type: ignore # mypy cannot yet infer it
|
||||
return exit_code # type: ignore
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
|
||||
|
||||
REQUIREMENTS = ['pyotp==2.2.6']
|
||||
REQUIREMENTS = ['pyotp==2.2.7']
|
||||
|
||||
CONF_MESSAGE = 'message'
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
|
||||
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
|
||||
|
||||
REQUIREMENTS = ['pyotp==2.2.6', 'PyQRCode==1.2.1']
|
||||
REQUIREMENTS = ['pyotp==2.2.7', 'PyQRCode==1.2.1']
|
||||
|
||||
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
@@ -11,6 +11,7 @@ from .models import PermissionLookup
|
||||
from .types import PolicyType
|
||||
from .entities import ENTITY_POLICY_SCHEMA, compile_entities
|
||||
from .merge import merge_policies # noqa
|
||||
from .util import test_all
|
||||
|
||||
|
||||
POLICY_SCHEMA = vol.Schema({
|
||||
@@ -29,6 +30,10 @@ class AbstractPermissions:
|
||||
"""Return a function that can test entity access."""
|
||||
raise NotImplementedError
|
||||
|
||||
def access_all_entities(self, key: str) -> bool:
|
||||
"""Check if we have a certain access to all entities."""
|
||||
raise NotImplementedError
|
||||
|
||||
def check_entity(self, entity_id: str, key: str) -> bool:
|
||||
"""Check if we can access entity."""
|
||||
entity_func = self._cached_entity_func
|
||||
@@ -48,6 +53,10 @@ class PolicyPermissions(AbstractPermissions):
|
||||
self._policy = policy
|
||||
self._perm_lookup = perm_lookup
|
||||
|
||||
def access_all_entities(self, key: str) -> bool:
|
||||
"""Check if we have a certain access to all entities."""
|
||||
return test_all(self._policy.get(CAT_ENTITIES), key)
|
||||
|
||||
def _entity_func(self) -> Callable[[str, str], bool]:
|
||||
"""Return a function that can test entity access."""
|
||||
return compile_entities(self._policy.get(CAT_ENTITIES),
|
||||
@@ -65,6 +74,10 @@ class _OwnerPermissions(AbstractPermissions):
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
def access_all_entities(self, key: str) -> bool:
|
||||
"""Check if we have a certain access to all entities."""
|
||||
return True
|
||||
|
||||
def _entity_func(self) -> Callable[[str, str], bool]:
|
||||
"""Return a function that can test entity access."""
|
||||
return lambda entity_id, key: True
|
||||
|
||||
@@ -3,6 +3,7 @@ from functools import wraps
|
||||
|
||||
from typing import Callable, Dict, List, Optional, Union, cast # noqa: F401
|
||||
|
||||
from .const import SUBCAT_ALL
|
||||
from .models import PermissionLookup
|
||||
from .types import CategoryType, SubCategoryDict, ValueType
|
||||
|
||||
@@ -96,3 +97,16 @@ def _gen_dict_test_func(
|
||||
return schema.get(key)
|
||||
|
||||
return test_value
|
||||
|
||||
|
||||
def test_all(policy: CategoryType, key: str) -> bool:
|
||||
"""Test if a policy has an ALL access for a specific key."""
|
||||
if not isinstance(policy, dict):
|
||||
return bool(policy)
|
||||
|
||||
all_policy = policy.get(SUBCAT_ALL)
|
||||
|
||||
if not isinstance(all_policy, dict):
|
||||
return bool(all_policy)
|
||||
|
||||
return all_policy.get(key, False)
|
||||
|
||||
@@ -26,6 +26,7 @@ ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
# hass.data key for logging information.
|
||||
DATA_LOGGING = 'logging'
|
||||
|
||||
DEBUGGER_INTEGRATIONS = {'ptvsd', }
|
||||
CORE_INTEGRATIONS = ('homeassistant', 'persistent_notification')
|
||||
LOGGING_INTEGRATIONS = {'logger', 'system_log'}
|
||||
STAGE_1_INTEGRATIONS = {
|
||||
@@ -93,6 +94,13 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
stop = time()
|
||||
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
|
||||
|
||||
if sys.version_info[:3] < (3, 6, 0):
|
||||
hass.components.persistent_notification.async_create(
|
||||
"Python 3.5 support is deprecated and will "
|
||||
"be removed in the first release after August 1. Please "
|
||||
"upgrade Python.", "Python version", "python_version"
|
||||
)
|
||||
|
||||
# TEMP: warn users for invalid slugs
|
||||
# Remove after 0.94 or 1.0
|
||||
if cv.INVALID_SLUGS_FOUND or cv.INVALID_ENTITY_IDS_FOUND:
|
||||
@@ -306,6 +314,15 @@ async def _async_set_up_integrations(
|
||||
"""Set up all the integrations."""
|
||||
domains = _get_domains(hass, config)
|
||||
|
||||
# Start up debuggers. Start these first in case they want to wait.
|
||||
debuggers = domains & DEBUGGER_INTEGRATIONS
|
||||
if debuggers:
|
||||
_LOGGER.debug("Starting up debuggers %s", debuggers)
|
||||
await asyncio.gather(*[
|
||||
async_setup_component(hass, domain, config)
|
||||
for domain in debuggers])
|
||||
domains -= DEBUGGER_INTEGRATIONS
|
||||
|
||||
# Resolve all dependencies of all components so we can find the logging
|
||||
# and integrations that need faster initialization.
|
||||
resolved_domains_task = asyncio.gather(*[
|
||||
@@ -339,7 +356,7 @@ async def _async_set_up_integrations(
|
||||
stage_2_domains = domains - logging_domains - stage_1_domains
|
||||
|
||||
if logging_domains:
|
||||
_LOGGER.debug("Setting up %s", logging_domains)
|
||||
_LOGGER.info("Setting up %s", logging_domains)
|
||||
|
||||
await asyncio.gather(*[
|
||||
async_setup_component(hass, domain, config)
|
||||
|
||||
@@ -31,9 +31,11 @@ CONF_ADS_TYPE = 'adstype'
|
||||
CONF_ADS_VALUE = 'value'
|
||||
CONF_ADS_VAR = 'adsvar'
|
||||
CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness'
|
||||
CONF_ADS_VAR_POSITION = 'adsvar_position'
|
||||
|
||||
STATE_KEY_STATE = 'state'
|
||||
STATE_KEY_BRIGHTNESS = 'brightness'
|
||||
STATE_KEY_POSITION = 'position'
|
||||
|
||||
DOMAIN = 'ads'
|
||||
|
||||
|
||||
165
homeassistant/components/ads/cover.py
Normal file
165
homeassistant/components/ads/cover.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""Support for ADS covers."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP,
|
||||
SUPPORT_SET_POSITION, ATTR_POSITION, DEVICE_CLASSES_SCHEMA,
|
||||
CoverDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_DEVICE_CLASS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import CONF_ADS_VAR, CONF_ADS_VAR_POSITION, DATA_ADS, \
|
||||
AdsEntity, STATE_KEY_STATE, STATE_KEY_POSITION
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'ADS Cover'
|
||||
|
||||
CONF_ADS_VAR_SET_POS = 'adsvar_set_position'
|
||||
CONF_ADS_VAR_OPEN = 'adsvar_open'
|
||||
CONF_ADS_VAR_CLOSE = 'adsvar_close'
|
||||
CONF_ADS_VAR_STOP = 'adsvar_stop'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_ADS_VAR): cv.string,
|
||||
vol.Optional(CONF_ADS_VAR_POSITION): cv.string,
|
||||
vol.Optional(CONF_ADS_VAR_SET_POS): cv.string,
|
||||
vol.Optional(CONF_ADS_VAR_CLOSE): cv.string,
|
||||
vol.Optional(CONF_ADS_VAR_OPEN): cv.string,
|
||||
vol.Optional(CONF_ADS_VAR_STOP): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the cover platform for ADS."""
|
||||
ads_hub = hass.data[DATA_ADS]
|
||||
|
||||
ads_var_is_closed = config.get(CONF_ADS_VAR)
|
||||
ads_var_position = config.get(CONF_ADS_VAR_POSITION)
|
||||
ads_var_pos_set = config.get(CONF_ADS_VAR_SET_POS)
|
||||
ads_var_open = config.get(CONF_ADS_VAR_OPEN)
|
||||
ads_var_close = config.get(CONF_ADS_VAR_CLOSE)
|
||||
ads_var_stop = config.get(CONF_ADS_VAR_STOP)
|
||||
name = config[CONF_NAME]
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
add_entities([AdsCover(ads_hub,
|
||||
ads_var_is_closed,
|
||||
ads_var_position,
|
||||
ads_var_pos_set,
|
||||
ads_var_open,
|
||||
ads_var_close,
|
||||
ads_var_stop,
|
||||
name,
|
||||
device_class)])
|
||||
|
||||
|
||||
class AdsCover(AdsEntity, CoverDevice):
|
||||
"""Representation of ADS cover."""
|
||||
|
||||
def __init__(self, ads_hub,
|
||||
ads_var_is_closed, ads_var_position,
|
||||
ads_var_pos_set, ads_var_open,
|
||||
ads_var_close, ads_var_stop, name, device_class):
|
||||
"""Initialize AdsCover entity."""
|
||||
super().__init__(ads_hub, name, ads_var_is_closed)
|
||||
if self._ads_var is None:
|
||||
if ads_var_position is not None:
|
||||
self._unique_id = ads_var_position
|
||||
elif ads_var_pos_set is not None:
|
||||
self._unique_id = ads_var_pos_set
|
||||
elif ads_var_open is not None:
|
||||
self._unique_id = ads_var_open
|
||||
|
||||
self._state_dict[STATE_KEY_POSITION] = None
|
||||
self._ads_var_position = ads_var_position
|
||||
self._ads_var_pos_set = ads_var_pos_set
|
||||
self._ads_var_open = ads_var_open
|
||||
self._ads_var_close = ads_var_close
|
||||
self._ads_var_stop = ads_var_stop
|
||||
self._device_class = device_class
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register device notification."""
|
||||
if self._ads_var is not None:
|
||||
await self.async_initialize_device(self._ads_var,
|
||||
self._ads_hub.PLCTYPE_BOOL)
|
||||
|
||||
if self._ads_var_position is not None:
|
||||
await self.async_initialize_device(self._ads_var_position,
|
||||
self._ads_hub.PLCTYPE_BYTE,
|
||||
STATE_KEY_POSITION)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this cover."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self._ads_var is not None:
|
||||
return self._state_dict[STATE_KEY_STATE]
|
||||
if self._ads_var_position is not None:
|
||||
return self._state_dict[STATE_KEY_POSITION] == 0
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return current position of cover."""
|
||||
return self._state_dict[STATE_KEY_POSITION]
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
if self._ads_var_stop is not None:
|
||||
supported_features |= SUPPORT_STOP
|
||||
|
||||
if self._ads_var_pos_set is not None:
|
||||
supported_features |= SUPPORT_SET_POSITION
|
||||
|
||||
return supported_features
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Fire the stop action."""
|
||||
if self._ads_var_stop:
|
||||
self._ads_hub.write_by_name(self._ads_var_stop, True,
|
||||
self._ads_hub.PLCTYPE_BOOL)
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Set cover position."""
|
||||
position = kwargs[ATTR_POSITION]
|
||||
if self._ads_var_pos_set is not None:
|
||||
self._ads_hub.write_by_name(self._ads_var_pos_set, position,
|
||||
self._ads_hub.PLCTYPE_BYTE)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Move the cover up."""
|
||||
if self._ads_var_open is not None:
|
||||
self._ads_hub.write_by_name(self._ads_var_open, True,
|
||||
self._ads_hub.PLCTYPE_BOOL)
|
||||
elif self._ads_var_pos_set is not None:
|
||||
self.set_cover_position(position=100)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Move the cover down."""
|
||||
if self._ads_var_close is not None:
|
||||
self._ads_hub.write_by_name(self._ads_var_close, True,
|
||||
self._ads_hub.PLCTYPE_BOOL)
|
||||
elif self._ads_var_pos_set is not None:
|
||||
self.set_cover_position(position=0)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return False if state has not been updated yet."""
|
||||
if self._ads_var is not None or self._ads_var_position is not None:
|
||||
return self._state_dict[STATE_KEY_STATE] is not None or \
|
||||
self._state_dict[STATE_KEY_POSITION] is not None
|
||||
return True
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Support for repeating alerts when conditions are met."""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.const import (
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers import service, event
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.util.dt import now
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -117,7 +118,7 @@ async def async_setup(hass, config):
|
||||
|
||||
tasks = [alert.async_update_ha_state() for alert in entities]
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
return True
|
||||
|
||||
@@ -222,7 +223,7 @@ class Alert(ToggleEntity):
|
||||
async def _schedule_notify(self):
|
||||
"""Schedule a notification."""
|
||||
delay = self._delay[self._next_delay]
|
||||
next_msg = datetime.now() + delay
|
||||
next_msg = now() + delay
|
||||
self._cancel = \
|
||||
event.async_track_point_in_time(self.hass, self._notify, next_msg)
|
||||
self._next_delay = min(self._next_delay + 1, len(self._delay) - 1)
|
||||
|
||||
@@ -39,7 +39,7 @@ class Auth:
|
||||
self._prefs = None
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
self._get_token_lock = asyncio.Lock(loop=hass.loop)
|
||||
self._get_token_lock = asyncio.Lock()
|
||||
|
||||
async def async_do_auth(self, accept_grant_code):
|
||||
"""Do authentication with an AcceptGrant code."""
|
||||
@@ -97,7 +97,7 @@ class Auth:
|
||||
|
||||
try:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self.hass.loop):
|
||||
with async_timeout.timeout(DEFAULT_TIMEOUT):
|
||||
response = await session.post(LWA_TOKEN_URI,
|
||||
headers=LWA_HEADERS,
|
||||
data=lwa_params,
|
||||
|
||||
@@ -449,9 +449,9 @@ class _AlexaPowerController(_AlexaInterface):
|
||||
if name != 'powerState':
|
||||
raise _UnsupportedProperty(name)
|
||||
|
||||
if self.entity.state == STATE_ON:
|
||||
return 'ON'
|
||||
return 'OFF'
|
||||
if self.entity.state == STATE_OFF:
|
||||
return 'OFF'
|
||||
return 'ON'
|
||||
|
||||
|
||||
class _AlexaLockController(_AlexaInterface):
|
||||
@@ -911,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:
|
||||
@@ -1428,7 +1432,7 @@ async def async_send_changereport_message(hass, config, alexa_entity):
|
||||
|
||||
try:
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(DEFAULT_TIMEOUT):
|
||||
response = await session.post(config.endpoint,
|
||||
headers=headers,
|
||||
json=message_serialized,
|
||||
|
||||
23
homeassistant/components/ambiclimate/.translations/ca.json
Normal file
23
homeassistant/components/ambiclimate/.translations/ca.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "S'ha produ\u00eft un error desconegut al generat un testimoni d'acc\u00e9s.",
|
||||
"already_setup": "El compte d\u2019Ambi Climate est\u00e0 configurat.",
|
||||
"no_config": "Necessites configurar Ambi Climate abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Autenticaci\u00f3 exitosa amb Ambi Climate."
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "V\u00e9s a l'enlla\u00e7 i autentica't abans de pr\u00e9mer Envia",
|
||||
"no_token": "No autenticat amb Ambi Climate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "V\u00e9s a l'[enlla\u00e7]({authorization_url}) i <b>Permet</b> l'acc\u00e9s al teu compte de Ambi Climate, despr\u00e9s torna i prem <b>Envia</b> (a sota).\n(Assegura't que l'enlla\u00e7 de retorn \u00e9s el seg\u00fcent {cb_url})",
|
||||
"title": "Autenticaci\u00f3 amb Ambi Climate"
|
||||
}
|
||||
},
|
||||
"title": "Ambi Climate"
|
||||
}
|
||||
}
|
||||
15
homeassistant/components/ambiclimate/.translations/cs.json
Normal file
15
homeassistant/components/ambiclimate/.translations/cs.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"follow_link": "N\u00e1sledujte odkaz a prove\u010fte ov\u011b\u0159en\u00ed p\u0159ed stisknut\u00edm tla\u010d\u00edtka Odeslat.",
|
||||
"no_token": "Nen\u00ed ov\u011b\u0159en s Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "N\u00e1sledujte tento [odkaz]({authorization_url}) a <b> Povolit </b> p\u0159\u00edstup k va\u0161emu \u00fa\u010dtu Ambiclimate, pot\u00e9 se vra\u0165te a stiskn\u011bte <b> Odeslat </b> n\u00ed\u017ee. \n (Ujist\u011bte se, \u017ee zadan\u00e1 adresa URL zp\u011btn\u00e9ho vol\u00e1n\u00ed je {cb_url} )",
|
||||
"title": "Ov\u011b\u0159it Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
||||
23
homeassistant/components/ambiclimate/.translations/de.json
Normal file
23
homeassistant/components/ambiclimate/.translations/de.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Unbekannter Fehler beim Generieren eines Zugriffstokens.",
|
||||
"already_setup": "Das Ambiclimate Konto ist konfiguriert.",
|
||||
"no_config": "Ambiclimate muss konfiguriert sein, bevor die Authentifizierund durchgef\u00fchrt werden kann. [Bitte lies die Anleitung] (https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Erfolgreiche Authentifizierung mit Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Bitte folge dem Link und authentifizieren dich, bevor du auf Senden klickst",
|
||||
"no_token": "Nicht authentifiziert mit Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Bitte folge diesem [link] ({authorization_url}) und <b> Erlaube </b> Zugriff auf dein Ambiclimate-Konto, komme dann zur\u00fcck und dr\u00fccke <b> Senden </b> darunter.\n (Pr\u00fcfe, dass die Callback-URL {cb_url} ist.)",
|
||||
"title": "Ambiclimate authentifizieren"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
||||
23
homeassistant/components/ambiclimate/.translations/en.json
Normal file
23
homeassistant/components/ambiclimate/.translations/en.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Unknown error generating an access token.",
|
||||
"already_setup": "The Ambiclimate account is configured.",
|
||||
"no_config": "You need to configure Ambiclimate before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Successfully authenticated with Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Please follow the link and authenticate before pressing Submit",
|
||||
"no_token": "Not authenticated with Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Please follow this [link]({authorization_url}) and <b>Allow</b> access to your Ambiclimate account, then come back and press <b>Submit</b> below.\n(Make sure the specified callback url is {cb_url})",
|
||||
"title": "Authenticate Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
||||
23
homeassistant/components/ambiclimate/.translations/es.json
Normal file
23
homeassistant/components/ambiclimate/.translations/es.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Error desconocido al generar un token de acceso.",
|
||||
"already_setup": "La cuenta de Ambiclimate est\u00e1 configurada.",
|
||||
"no_config": "Es necesario configurar Ambiclimate antes de poder autenticarse con \u00e9l. [Por favor, lee las instrucciones](https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Autenticado correctamente con Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Accede al enlace e identif\u00edcate antes de pulsar Enviar.",
|
||||
"no_token": "No autenticado con Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Accede al siguiente [enlace]({authorization_url}) y <b>permite</b> el acceso a tu cuenta de Ambiclimate, despu\u00e9s vuelve y pulsa en <b>enviar</b> a continuaci\u00f3n.\n(Aseg\u00farate que la url de devoluci\u00f3n de llamada es {cb_url})",
|
||||
"title": "Autenticaci\u00f3n de Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
||||
23
homeassistant/components/ambiclimate/.translations/fr.json
Normal file
23
homeassistant/components/ambiclimate/.translations/fr.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'un jeton d'acc\u00e8s.",
|
||||
"already_setup": "Le compte Ambiclimate est configur\u00e9.",
|
||||
"no_config": "Vous devez configurer Ambiclimate avant de pouvoir vous authentifier aupr\u00e8s de celui-ci. [Veuillez lire les instructions] (https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Authentifi\u00e9 avec succ\u00e8s avec Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.",
|
||||
"no_token": "Non authentifi\u00e9 avec Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Suivez ce [lien] ( {authorization_url} ) et <b> Autorisez </b> l'acc\u00e8s \u00e0 votre compte Ambiclimate, puis revenez et appuyez sur <b> Envoyer </b> ci-dessous. \n (Assurez-vous que l'URL de rappel sp\u00e9cifi\u00e9 est {cb_url} )",
|
||||
"title": "Authentifier Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
||||
23
homeassistant/components/ambiclimate/.translations/ko.json
Normal file
23
homeassistant/components/ambiclimate/.translations/ko.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070 \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
|
||||
"already_setup": "Ambi Climate \uacc4\uc815\uc774 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
|
||||
"no_config": "Ambi Climate \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Ambi Climate \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/ambiclimate/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Ambi Climate \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Submit \ubc84\ud2bc\uc744 \ub204\ub974\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694",
|
||||
"no_token": "Ambi Climate \ub85c \uc778\uc99d\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "[\ub9c1\ud06c]({authorization_url}) \ub97c \ud074\ub9ad\ud558\uc5ec Ambi Climate \uacc4\uc815\uc5d0 \ub300\ud574 <b>\ud5c8\uc6a9</b> \ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 <b>Submit</b> \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694. \n(\ucf5c\ubc31 url \uc744 {cb_url} \ub85c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694)",
|
||||
"title": "Ambi Climate \uc778\uc99d"
|
||||
}
|
||||
},
|
||||
"title": "Ambi Climate"
|
||||
}
|
||||
}
|
||||
23
homeassistant/components/ambiclimate/.translations/lb.json
Normal file
23
homeassistant/components/ambiclimate/.translations/lb.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Onbekannte Feeler beim gener\u00e9ieren vum Acc\u00e8s Jeton.",
|
||||
"already_setup": "Den Ambiclimate Kont ass konfigur\u00e9iert.",
|
||||
"no_config": "Dir musst Ambiclimate konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen.[Liest w.e.g. d'Instruktioune](https://www.home-assistant.io/components/ambiclimatet/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Erfollegr\u00e4ich mat Ambiclimate authentifiz\u00e9iert."
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Follegt w.e.g. dem Link an authentifiz\u00e9iert de Kont ier dir op ofsch\u00e9cken dr\u00e9ckt.",
|
||||
"no_token": "Net mat Ambiclimate authentifiz\u00e9iert"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Follegt d\u00ebsem [Link]({authorization_url}) an <b>erlaabtt</b> den Acc\u00e8s zu \u00e4rem Ambiclimate Kont , a kommt dann zer\u00e9ck heihin an dr\u00e9ck op <b>ofsch\u00e9cken</b> hei \u00ebnnen.\n(Stellt s\u00e9cher dass den Type vun Callback {cb_url} ass.)",
|
||||
"title": "Ambiclimate authentifiz\u00e9ieren"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
||||
23
homeassistant/components/ambiclimate/.translations/no.json
Normal file
23
homeassistant/components/ambiclimate/.translations/no.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Ukjent feil ved oppretting av tilgangstoken.",
|
||||
"already_setup": "Ambiclimate-kontoen er konfigurert.",
|
||||
"no_config": "Du m\u00e5 konfigurere Ambiclimate f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Vellykket autentisering med Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Vennligst f\u00f8lg lenken og godkjen f\u00f8r du trykker p\u00e5 Send",
|
||||
"no_token": "Ikke autentisert med Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Vennligst f\u00f8lg denne [linken]({authorization_url}) og <b>Tillat</b> tilgang til din Ambiclimate konto, og kom s\u00e5 tilbake og trykk <b>Send</b> nedenfor.\n(Kontroller at den angitte URL-adressen for tilbakeringing er {cb_url})",
|
||||
"title": "Autensiere Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
||||
23
homeassistant/components/ambiclimate/.translations/pl.json
Normal file
23
homeassistant/components/ambiclimate/.translations/pl.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Nieznany b\u0142\u0105d podczas generowania tokena dost\u0119pu.",
|
||||
"already_setup": "Konto Ambiclimate jest skonfigurowane.",
|
||||
"no_config": "Musisz skonfigurowa\u0107 Ambiclimate, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Pomy\u015blnie uwierzytelniono z Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Prosz\u0119 klikn\u0105\u0107 link i uwierzytelni\u0107 przed naci\u015bni\u0119ciem przycisku Prze\u015blij",
|
||||
"no_token": "Nie uwierzytelniony z Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Kliknij poni\u017cszy [link]({authorization_url}) i <b>Zezw\u00f3l</b> na dost\u0119p do swojego konta Ambiclimate, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij <b>Prze\u015blij</b> poni\u017cej. \n(Upewnij si\u0119, \u017ce podany adres URL to {cb_url})",
|
||||
"title": "Uwierzytelnienie Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
||||
23
homeassistant/components/ambiclimate/.translations/ru.json
Normal file
23
homeassistant/components/ambiclimate/.translations/ru.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.",
|
||||
"already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Ambi Climate \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.",
|
||||
"no_config": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Ambi Climate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e, \u043f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043d\u0430\u0436\u0430\u0442\u044c \"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\".",
|
||||
"no_token": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430."
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 <b>\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435</b> \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Ambi Climate, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 <b>\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c</b>. \n(\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 {cb_url})",
|
||||
"title": "Ambi Climate"
|
||||
}
|
||||
},
|
||||
"title": "Ambi Climate"
|
||||
}
|
||||
}
|
||||
23
homeassistant/components/ambiclimate/.translations/sl.json
Normal file
23
homeassistant/components/ambiclimate/.translations/sl.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Neznana napaka pri ustvarjanju \u017eetona za dostop.",
|
||||
"already_setup": "Ra\u010dun Ambiclimate je konfiguriran.",
|
||||
"no_config": "Ambiclimat morate konfigurirati, preden lahko z njo preverjate pristnost. [Preberite navodila] (https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Uspe\u0161no overjeno z funkcijo Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Preden pritisnete Po\u0161lji, sledite povezavi in preverite pristnost",
|
||||
"no_token": "Ni overjeno z Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Sledite temu povezavi ( {authorization_url} in <b> Dovoli </b> dostopu do svojega ra\u010duna Ambiclimate, nato se vrnite in pritisnite <b> Po\u0161lji </b> spodaj. \n (Poskrbite, da je dolo\u010den url za povratni klic {cb_url} )",
|
||||
"title": "Overi Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
||||
23
homeassistant/components/ambiclimate/.translations/sv.json
Normal file
23
homeassistant/components/ambiclimate/.translations/sv.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Ok\u00e4nt fel vid generering av \u00e5tkomsttoken.",
|
||||
"already_setup": "Ambiclientkontot \u00e4r konfigurerat",
|
||||
"no_config": "Du m\u00e5ste konfigurera Ambiclimate innan du kan autentisera med den. [V\u00e4nligen l\u00e4s instruktionerna] (https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Lyckad autentisering med Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "V\u00e4nligen f\u00f6lj l\u00e4nken och autentisera dig innan du trycker p\u00e5 Skicka",
|
||||
"no_token": "Inte autentiserad med Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "V\u00e4nligen f\u00f6lj denna [l\u00e4nk] ({authorization_url}) och <b> till\u00e5ta </b> till g\u00e5ng till ditt Ambiclimate konto, kom sedan tillbaka och tryck p\u00e5 <b> Skicka </b> nedan.\n(Kontrollera att den angivna callback url \u00e4r {cb_url})",
|
||||
"title": "Autentisera Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "\u7522\u751f\u5b58\u53d6\u8a8d\u8b49\u78bc\u672a\u77e5\u932f\u8aa4\u3002",
|
||||
"already_setup": "Ambiclimate \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
|
||||
"no_config": "\u5fc5\u9808\u5148\u8a2d\u5b9a Ambiclimate \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/ambiclimate/\uff09\u3002"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "\u5df2\u6210\u529f\u8a8d\u8b49 Ambiclimate \u88dd\u7f6e\u3002"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "\u8acb\u65bc\u50b3\u9001\u524d\uff0c\u5148\u4f7f\u7528\u9023\u7d50\u4e26\u9032\u884c\u8a8d\u8b49\u3002",
|
||||
"no_token": "Ambiclimate \u672a\u6388\u6b0a"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "\u8acb\u4f7f\u7528\u6b64[\u9023\u7d50]\uff08{authorization_url}\uff09\u4e26\u9ede\u9078<b>\u5141\u8a31</b>\u4ee5\u5b58\u53d6 Ambiclimate \u5e33\u865f\uff0c\u7136\u5f8c\u8fd4\u56de\u6b64\u9801\u9762\u4e26\u9ede\u9078\u4e0b\u65b9\u7684<b>\u50b3\u9001</b>\u3002\n\uff08\u78ba\u5b9a Callback url \u70ba {cb_url}\uff09",
|
||||
"title": "\u8a8d\u8b49 Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
||||
44
homeassistant/components/ambiclimate/__init__.py
Normal file
44
homeassistant/components/ambiclimate/__init__.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Support for Ambiclimate devices."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from . import config_flow
|
||||
from .const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, DOMAIN
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN:
|
||||
vol.Schema({
|
||||
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
||||
})
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up Ambiclimate components."""
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
conf = config[DOMAIN]
|
||||
|
||||
config_flow.register_flow_implementation(
|
||||
hass, conf[CONF_CLIENT_ID],
|
||||
conf[CONF_CLIENT_SECRET])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up Ambiclimate from a config entry."""
|
||||
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
||||
entry, 'climate'))
|
||||
|
||||
return True
|
||||
230
homeassistant/components/ambiclimate/climate.py
Normal file
230
homeassistant/components/ambiclimate/climate.py
Normal file
@@ -0,0 +1,230 @@
|
||||
"""Support for Ambiclimate ac."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import ambiclimate
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_ON_OFF, STATE_HEAT)
|
||||
from homeassistant.const import ATTR_NAME
|
||||
from homeassistant.const import (ATTR_TEMPERATURE,
|
||||
STATE_OFF, TEMP_CELSIUS)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from .const import (ATTR_VALUE, CONF_CLIENT_ID, CONF_CLIENT_SECRET,
|
||||
DOMAIN, SERVICE_COMFORT_FEEDBACK, SERVICE_COMFORT_MODE,
|
||||
SERVICE_TEMPERATURE_MODE, STORAGE_KEY, STORAGE_VERSION)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
|
||||
SUPPORT_ON_OFF)
|
||||
|
||||
SEND_COMFORT_FEEDBACK_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_NAME): cv.string,
|
||||
vol.Required(ATTR_VALUE): cv.string,
|
||||
})
|
||||
|
||||
SET_COMFORT_MODE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_NAME): cv.string,
|
||||
})
|
||||
|
||||
SET_TEMPERATURE_MODE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_NAME): cv.string,
|
||||
vol.Required(ATTR_VALUE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the Ambicliamte device."""
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up the Ambicliamte device from config entry."""
|
||||
config = entry.data
|
||||
websession = async_get_clientsession(hass)
|
||||
store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
token_info = await store.async_load()
|
||||
|
||||
oauth = ambiclimate.AmbiclimateOAuth(config[CONF_CLIENT_ID],
|
||||
config[CONF_CLIENT_SECRET],
|
||||
config['callback_url'],
|
||||
websession)
|
||||
|
||||
try:
|
||||
_token_info = await oauth.refresh_access_token(token_info)
|
||||
except ambiclimate.AmbiclimateOauthError:
|
||||
_LOGGER.error("Failed to refresh access token")
|
||||
return
|
||||
|
||||
if _token_info:
|
||||
await store.async_save(_token_info)
|
||||
token_info = _token_info
|
||||
|
||||
data_connection = ambiclimate.AmbiclimateConnection(oauth,
|
||||
token_info=token_info,
|
||||
websession=websession)
|
||||
|
||||
if not await data_connection.find_devices():
|
||||
_LOGGER.error("No devices found")
|
||||
return
|
||||
|
||||
tasks = []
|
||||
for heater in data_connection.get_devices():
|
||||
tasks.append(heater.update_device_info())
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
devs = []
|
||||
for heater in data_connection.get_devices():
|
||||
devs.append(AmbiclimateEntity(heater, store))
|
||||
|
||||
async_add_entities(devs, True)
|
||||
|
||||
async def send_comfort_feedback(service):
|
||||
"""Send comfort feedback."""
|
||||
device_name = service.data[ATTR_NAME]
|
||||
device = data_connection.find_device_by_room_name(device_name)
|
||||
if device:
|
||||
await device.set_comfort_feedback(service.data[ATTR_VALUE])
|
||||
|
||||
hass.services.async_register(DOMAIN,
|
||||
SERVICE_COMFORT_FEEDBACK,
|
||||
send_comfort_feedback,
|
||||
schema=SEND_COMFORT_FEEDBACK_SCHEMA)
|
||||
|
||||
async def set_comfort_mode(service):
|
||||
"""Set comfort mode."""
|
||||
device_name = service.data[ATTR_NAME]
|
||||
device = data_connection.find_device_by_room_name(device_name)
|
||||
if device:
|
||||
await device.set_comfort_mode()
|
||||
|
||||
hass.services.async_register(DOMAIN,
|
||||
SERVICE_COMFORT_MODE,
|
||||
set_comfort_mode,
|
||||
schema=SET_COMFORT_MODE_SCHEMA)
|
||||
|
||||
async def set_temperature_mode(service):
|
||||
"""Set temperature mode."""
|
||||
device_name = service.data[ATTR_NAME]
|
||||
device = data_connection.find_device_by_room_name(device_name)
|
||||
if device:
|
||||
await device.set_temperature_mode(service.data[ATTR_VALUE])
|
||||
|
||||
hass.services.async_register(DOMAIN,
|
||||
SERVICE_TEMPERATURE_MODE,
|
||||
set_temperature_mode,
|
||||
schema=SET_TEMPERATURE_MODE_SCHEMA)
|
||||
|
||||
|
||||
class AmbiclimateEntity(ClimateDevice):
|
||||
"""Representation of a Ambiclimate Thermostat device."""
|
||||
|
||||
def __init__(self, heater, store):
|
||||
"""Initialize the thermostat."""
|
||||
self._heater = heater
|
||||
self._store = store
|
||||
self._data = {}
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._heater.device_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._heater.name
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info."""
|
||||
return {
|
||||
'identifiers': {
|
||||
(DOMAIN, self.unique_id)
|
||||
},
|
||||
'name': self.name,
|
||||
'manufacturer': 'Ambiclimate',
|
||||
}
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement which this thermostat uses."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the target temperature."""
|
||||
return self._data.get('target_temperature')
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return 1
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._data.get('temperature')
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity."""
|
||||
return self._data.get('humidity')
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if heater is on."""
|
||||
return self._data.get('power', '').lower() == 'on'
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self._heater.get_min_temp()
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._heater.get_max_temp()
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation."""
|
||||
return STATE_HEAT if self.is_on else STATE_OFF
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
await self._heater.set_target_temperature(temperature)
|
||||
|
||||
async def async_turn_on(self):
|
||||
"""Turn device on."""
|
||||
await self._heater.turn_on()
|
||||
|
||||
async def async_turn_off(self):
|
||||
"""Turn device off."""
|
||||
await self._heater.turn_off()
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
try:
|
||||
token_info = await self._heater.control.refresh_access_token()
|
||||
except ambiclimate.AmbiclimateOauthError:
|
||||
_LOGGER.error("Failed to refresh access token")
|
||||
return
|
||||
|
||||
if token_info:
|
||||
await self._store.async_save(token_info)
|
||||
|
||||
self._data = await self._heater.update_device()
|
||||
153
homeassistant/components/ambiclimate/config_flow.py
Normal file
153
homeassistant/components/ambiclimate/config_flow.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""Config flow for Ambiclimate."""
|
||||
import logging
|
||||
|
||||
import ambiclimate
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from .const import (AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, CONF_CLIENT_ID,
|
||||
CONF_CLIENT_SECRET, DOMAIN, STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
DATA_AMBICLIMATE_IMPL = 'ambiclimate_flow_implementation'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def register_flow_implementation(hass, client_id, client_secret):
|
||||
"""Register a ambiclimate implementation.
|
||||
|
||||
client_id: Client id.
|
||||
client_secret: Client secret.
|
||||
"""
|
||||
hass.data.setdefault(DATA_AMBICLIMATE_IMPL, {})
|
||||
|
||||
hass.data[DATA_AMBICLIMATE_IMPL] = {
|
||||
CONF_CLIENT_ID: client_id,
|
||||
CONF_CLIENT_SECRET: client_secret,
|
||||
}
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register('ambiclimate')
|
||||
class AmbiclimateFlowHandler(config_entries.ConfigFlow):
|
||||
"""Handle a config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize flow."""
|
||||
self._registered_view = False
|
||||
self._oauth = None
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle external yaml configuration."""
|
||||
if self.hass.config_entries.async_entries(DOMAIN):
|
||||
return self.async_abort(reason='already_setup')
|
||||
|
||||
config = self.hass.data.get(DATA_AMBICLIMATE_IMPL, {})
|
||||
|
||||
if not config:
|
||||
_LOGGER.debug("No config")
|
||||
return self.async_abort(reason='no_config')
|
||||
|
||||
return await self.async_step_auth()
|
||||
|
||||
async def async_step_auth(self, user_input=None):
|
||||
"""Handle a flow start."""
|
||||
if self.hass.config_entries.async_entries(DOMAIN):
|
||||
return self.async_abort(reason='already_setup')
|
||||
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
errors['base'] = 'follow_link'
|
||||
|
||||
if not self._registered_view:
|
||||
self._generate_view()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='auth',
|
||||
description_placeholders={'authorization_url':
|
||||
await self._get_authorize_url(),
|
||||
'cb_url': self._cb_url()},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_code(self, code=None):
|
||||
"""Received code for authentication."""
|
||||
if self.hass.config_entries.async_entries(DOMAIN):
|
||||
return self.async_abort(reason='already_setup')
|
||||
|
||||
token_info = await self._get_token_info(code)
|
||||
|
||||
if token_info is None:
|
||||
return self.async_abort(reason='access_token')
|
||||
|
||||
config = self.hass.data[DATA_AMBICLIMATE_IMPL].copy()
|
||||
config['callback_url'] = self._cb_url()
|
||||
|
||||
return self.async_create_entry(
|
||||
title="Ambiclimate",
|
||||
data=config,
|
||||
)
|
||||
|
||||
async def _get_token_info(self, code):
|
||||
oauth = self._generate_oauth()
|
||||
try:
|
||||
token_info = await oauth.get_access_token(code)
|
||||
except ambiclimate.AmbiclimateOauthError:
|
||||
_LOGGER.error("Failed to get access token", exc_info=True)
|
||||
return None
|
||||
|
||||
store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
await store.async_save(token_info)
|
||||
|
||||
return token_info
|
||||
|
||||
def _generate_view(self):
|
||||
self.hass.http.register_view(AmbiclimateAuthCallbackView())
|
||||
self._registered_view = True
|
||||
|
||||
def _generate_oauth(self):
|
||||
config = self.hass.data[DATA_AMBICLIMATE_IMPL]
|
||||
clientsession = async_get_clientsession(self.hass)
|
||||
callback_url = self._cb_url()
|
||||
|
||||
oauth = ambiclimate.AmbiclimateOAuth(config.get(CONF_CLIENT_ID),
|
||||
config.get(CONF_CLIENT_SECRET),
|
||||
callback_url,
|
||||
clientsession)
|
||||
return oauth
|
||||
|
||||
def _cb_url(self):
|
||||
return '{}{}'.format(self.hass.config.api.base_url,
|
||||
AUTH_CALLBACK_PATH)
|
||||
|
||||
async def _get_authorize_url(self):
|
||||
oauth = self._generate_oauth()
|
||||
return oauth.get_authorize_url()
|
||||
|
||||
|
||||
class AmbiclimateAuthCallbackView(HomeAssistantView):
|
||||
"""Ambiclimate Authorization Callback View."""
|
||||
|
||||
requires_auth = False
|
||||
url = AUTH_CALLBACK_PATH
|
||||
name = AUTH_CALLBACK_NAME
|
||||
|
||||
async def get(self, request):
|
||||
"""Receive authorization token."""
|
||||
code = request.query.get('code')
|
||||
if code is None:
|
||||
return "No code"
|
||||
hass = request.app['hass']
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={'source': 'code'},
|
||||
data=code,
|
||||
))
|
||||
return "OK!"
|
||||
14
homeassistant/components/ambiclimate/const.py
Normal file
14
homeassistant/components/ambiclimate/const.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Constants used by the Ambiclimate component."""
|
||||
|
||||
ATTR_VALUE = 'value'
|
||||
CONF_CLIENT_ID = 'client_id'
|
||||
CONF_CLIENT_SECRET = 'client_secret'
|
||||
DOMAIN = 'ambiclimate'
|
||||
SERVICE_COMFORT_FEEDBACK = 'send_comfort_feedback'
|
||||
SERVICE_COMFORT_MODE = 'set_comfort_mode'
|
||||
SERVICE_TEMPERATURE_MODE = 'set_temperature_mode'
|
||||
STORAGE_KEY = 'ambiclimate_auth'
|
||||
STORAGE_VERSION = 1
|
||||
|
||||
AUTH_CALLBACK_NAME = 'api:ambiclimate'
|
||||
AUTH_CALLBACK_PATH = '/api/ambiclimate'
|
||||
13
homeassistant/components/ambiclimate/manifest.json
Normal file
13
homeassistant/components/ambiclimate/manifest.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "ambiclimate",
|
||||
"name": "Ambiclimate",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/ambiclimate",
|
||||
"requirements": [
|
||||
"ambiclimate==0.1.2"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@danielhiversen"
|
||||
]
|
||||
}
|
||||
36
homeassistant/components/ambiclimate/services.yaml
Normal file
36
homeassistant/components/ambiclimate/services.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
# Describes the format for available services for ambiclimate
|
||||
|
||||
set_comfort_mode:
|
||||
description: >
|
||||
Enable comfort mode on your AC
|
||||
fields:
|
||||
Name:
|
||||
description: >
|
||||
String with device name.
|
||||
example: Bedroom
|
||||
|
||||
send_comfort_feedback:
|
||||
description: >
|
||||
Send feedback for comfort mode
|
||||
fields:
|
||||
Name:
|
||||
description: >
|
||||
String with device name.
|
||||
example: Bedroom
|
||||
Value:
|
||||
description: >
|
||||
Send any of the following comfort values: too_hot, too_warm, bit_warm, comfortable, bit_cold, too_cold, freezing
|
||||
example: bit_warm
|
||||
|
||||
set_temperature_mode:
|
||||
description: >
|
||||
Enable temperature mode on your AC
|
||||
fields:
|
||||
Name:
|
||||
description: >
|
||||
String with device name.
|
||||
example: Bedroom
|
||||
Value:
|
||||
description: >
|
||||
Target value in celsius
|
||||
example: 22
|
||||
23
homeassistant/components/ambiclimate/strings.json
Normal file
23
homeassistant/components/ambiclimate/strings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "Ambiclimate",
|
||||
"step": {
|
||||
"auth": {
|
||||
"title": "Authenticate Ambiclimate",
|
||||
"description": "Please follow this [link]({authorization_url}) and <b>Allow</b> access to your Ambiclimate account, then come back and press <b>Submit</b> below.\n(Make sure the specified callback url is {cb_url})"
|
||||
}
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Successfully authenticated with Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"no_token": "Not authenticated with Ambiclimate",
|
||||
"follow_link": "Please follow the link and authenticate before pressing Submit"
|
||||
},
|
||||
"abort": {
|
||||
"already_setup": "The Ambiclimate account is configured.",
|
||||
"no_config": "You need to configure Ambiclimate before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/ambiclimate/).",
|
||||
"access_token": "Unknown error generating an access token."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"domain": "ambient_station",
|
||||
"name": "Ambient station",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/ambient_station",
|
||||
"requirements": [
|
||||
"aioambient==0.3.0"
|
||||
|
||||
@@ -5,16 +5,30 @@ from datetime import timedelta
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.permissions.const import POLICY_CONTROL
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
|
||||
from homeassistant.components.camera import DOMAIN as CAMERA
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL,
|
||||
HTTP_BASIC_AUTHENTICATION)
|
||||
ATTR_ENTITY_ID, CONF_AUTHENTICATION, CONF_BINARY_SENSORS, CONF_HOST,
|
||||
CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS,
|
||||
CONF_SWITCHES, CONF_USERNAME, ENTITY_MATCH_ALL, HTTP_BASIC_AUTHENTICATION)
|
||||
from homeassistant.exceptions import Unauthorized, UnknownUser
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.service import async_extract_entity_ids
|
||||
|
||||
from .binary_sensor import BINARY_SENSORS
|
||||
from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST
|
||||
from .const import DOMAIN, DATA_AMCREST
|
||||
from .helpers import service_signal
|
||||
from .sensor import SENSOR_MOTION_DETECTOR, SENSORS
|
||||
from .switch import SWITCHES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_AUTHENTICATION = 'authentication'
|
||||
CONF_RESOLUTION = 'resolution'
|
||||
CONF_STREAM_SOURCE = 'stream_source'
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
@@ -22,12 +36,7 @@ CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
DEFAULT_NAME = 'Amcrest Camera'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_RESOLUTION = 'high'
|
||||
DEFAULT_STREAM_SOURCE = 'snapshot'
|
||||
DEFAULT_ARGUMENTS = '-pred 1'
|
||||
TIMEOUT = 10
|
||||
|
||||
DATA_AMCREST = 'amcrest'
|
||||
DOMAIN = 'amcrest'
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
@@ -43,70 +52,60 @@ AUTHENTICATION_LIST = {
|
||||
'basic': 'basic'
|
||||
}
|
||||
|
||||
STREAM_SOURCE_LIST = {
|
||||
'mjpeg': 0,
|
||||
'snapshot': 1,
|
||||
'rtsp': 2,
|
||||
}
|
||||
|
||||
BINARY_SENSORS = {
|
||||
'motion_detected': 'Motion Detected'
|
||||
}
|
||||
|
||||
# Sensor types are defined like: Name, units, icon
|
||||
SENSOR_MOTION_DETECTOR = 'motion_detector'
|
||||
SENSORS = {
|
||||
SENSOR_MOTION_DETECTOR: ['Motion Detected', None, 'mdi:run'],
|
||||
'sdcard': ['SD Used', '%', 'mdi:sd'],
|
||||
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
|
||||
}
|
||||
|
||||
# Switch types are defined like: Name, icon
|
||||
SWITCHES = {
|
||||
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
|
||||
'motion_recording': ['Motion Recording', 'mdi:record-rec']
|
||||
}
|
||||
|
||||
|
||||
def _deprecated_sensors(value):
|
||||
if SENSOR_MOTION_DETECTOR in value:
|
||||
def _deprecated_sensor_values(sensors):
|
||||
if SENSOR_MOTION_DETECTOR in sensors:
|
||||
_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
|
||||
"The 'sensors' option value '%s' is deprecated, "
|
||||
"please remove it from your configuration and use "
|
||||
"the 'binary_sensors' option with value 'motion_detected' "
|
||||
"instead.", SENSOR_MOTION_DETECTOR)
|
||||
return sensors
|
||||
|
||||
|
||||
def _has_unique_names(value):
|
||||
names = [camera[CONF_NAME] for camera in value]
|
||||
def _deprecated_switches(config):
|
||||
if CONF_SWITCHES in config:
|
||||
_LOGGER.warning(
|
||||
"The 'switches' option (with value %s) is deprecated, "
|
||||
"please remove it from your configuration and use "
|
||||
"camera services and attributes instead.",
|
||||
config[CONF_SWITCHES])
|
||||
return config
|
||||
|
||||
|
||||
def _has_unique_names(devices):
|
||||
names = [device[CONF_NAME] for device in devices]
|
||||
vol.Schema(vol.Unique())(names)
|
||||
return value
|
||||
return devices
|
||||
|
||||
|
||||
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)]),
|
||||
})
|
||||
AMCREST_SCHEMA = vol.All(
|
||||
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=STREAM_SOURCE_LIST[0]):
|
||||
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_sensor_values),
|
||||
vol.Optional(CONF_SWITCHES):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
}),
|
||||
_deprecated_switches
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [AMCREST_SCHEMA], _has_unique_names)
|
||||
@@ -117,21 +116,22 @@ def setup(hass, config):
|
||||
"""Set up the Amcrest IP Camera component."""
|
||||
from amcrest import AmcrestCamera, AmcrestError
|
||||
|
||||
hass.data.setdefault(DATA_AMCREST, {})
|
||||
amcrest_cams = config[DOMAIN]
|
||||
hass.data.setdefault(DATA_AMCREST, {'devices': {}, 'cameras': []})
|
||||
devices = config[DOMAIN]
|
||||
|
||||
for device in amcrest_cams:
|
||||
for device in devices:
|
||||
name = device[CONF_NAME]
|
||||
username = device[CONF_USERNAME]
|
||||
password = device[CONF_PASSWORD]
|
||||
|
||||
try:
|
||||
camera = AmcrestCamera(device[CONF_HOST],
|
||||
device[CONF_PORT],
|
||||
username,
|
||||
password).camera
|
||||
api = AmcrestCamera(device[CONF_HOST],
|
||||
device[CONF_PORT],
|
||||
username,
|
||||
password).camera
|
||||
# pylint: disable=pointless-statement
|
||||
camera.current_time
|
||||
# Test camera communications.
|
||||
api.current_time
|
||||
|
||||
except AmcrestError as ex:
|
||||
_LOGGER.error("Unable to connect to %s camera: %s", name, str(ex))
|
||||
@@ -148,7 +148,7 @@ def setup(hass, config):
|
||||
binary_sensors = device.get(CONF_BINARY_SENSORS)
|
||||
sensors = device.get(CONF_SENSORS)
|
||||
switches = device.get(CONF_SWITCHES)
|
||||
stream_source = STREAM_SOURCE_LIST[device[CONF_STREAM_SOURCE]]
|
||||
stream_source = device[CONF_STREAM_SOURCE]
|
||||
|
||||
# currently aiohttp only works with basic authentication
|
||||
# only valid for mjpeg streaming
|
||||
@@ -157,47 +157,97 @@ def setup(hass, config):
|
||||
else:
|
||||
authentication = None
|
||||
|
||||
hass.data[DATA_AMCREST][name] = AmcrestDevice(
|
||||
camera, name, authentication, ffmpeg_arguments, stream_source,
|
||||
hass.data[DATA_AMCREST]['devices'][name] = AmcrestDevice(
|
||||
api, authentication, ffmpeg_arguments, stream_source,
|
||||
resolution)
|
||||
|
||||
discovery.load_platform(
|
||||
hass, 'camera', DOMAIN, {
|
||||
hass, CAMERA, DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
}, config)
|
||||
|
||||
if binary_sensors:
|
||||
discovery.load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {
|
||||
hass, BINARY_SENSOR, DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_BINARY_SENSORS: binary_sensors
|
||||
}, config)
|
||||
|
||||
if sensors:
|
||||
discovery.load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
hass, SENSOR, DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_SENSORS: sensors,
|
||||
}, config)
|
||||
|
||||
if switches:
|
||||
discovery.load_platform(
|
||||
hass, 'switch', DOMAIN, {
|
||||
hass, SWITCH, DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_SWITCHES: switches
|
||||
}, config)
|
||||
|
||||
return len(hass.data[DATA_AMCREST]) >= 1
|
||||
if not hass.data[DATA_AMCREST]['devices']:
|
||||
return False
|
||||
|
||||
def have_permission(user, entity_id):
|
||||
return not user or user.permissions.check_entity(
|
||||
entity_id, POLICY_CONTROL)
|
||||
|
||||
async def async_extract_from_service(call):
|
||||
if call.context.user_id:
|
||||
user = await hass.auth.async_get_user(call.context.user_id)
|
||||
if user is None:
|
||||
raise UnknownUser(context=call.context)
|
||||
else:
|
||||
user = None
|
||||
|
||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
|
||||
# Return all entity_ids user has permission to control.
|
||||
return [
|
||||
entity_id for entity_id in hass.data[DATA_AMCREST]['cameras']
|
||||
if have_permission(user, entity_id)
|
||||
]
|
||||
|
||||
call_ids = await async_extract_entity_ids(hass, call)
|
||||
entity_ids = []
|
||||
for entity_id in hass.data[DATA_AMCREST]['cameras']:
|
||||
if entity_id not in call_ids:
|
||||
continue
|
||||
if not have_permission(user, entity_id):
|
||||
raise Unauthorized(
|
||||
context=call.context,
|
||||
entity_id=entity_id,
|
||||
permission=POLICY_CONTROL
|
||||
)
|
||||
entity_ids.append(entity_id)
|
||||
return entity_ids
|
||||
|
||||
async def async_service_handler(call):
|
||||
args = []
|
||||
for arg in CAMERA_SERVICES[call.service][2]:
|
||||
args.append(call.data[arg])
|
||||
for entity_id in await async_extract_from_service(call):
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
service_signal(call.service, entity_id),
|
||||
*args
|
||||
)
|
||||
|
||||
for service, params in CAMERA_SERVICES.items():
|
||||
hass.services.async_register(
|
||||
DOMAIN, service, async_service_handler, params[0])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AmcrestDevice:
|
||||
"""Representation of a base Amcrest discovery device."""
|
||||
|
||||
def __init__(self, camera, name, authentication, ffmpeg_arguments,
|
||||
def __init__(self, api, authentication, ffmpeg_arguments,
|
||||
stream_source, resolution):
|
||||
"""Initialize the entity."""
|
||||
self.device = camera
|
||||
self.name = name
|
||||
self.api = api
|
||||
self.authentication = authentication
|
||||
self.ffmpeg_arguments = ffmpeg_arguments
|
||||
self.stream_source = stream_source
|
||||
|
||||
@@ -5,38 +5,39 @@ 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
|
||||
|
||||
from .const import BINARY_SENSOR_SCAN_INTERVAL_SECS, DATA_AMCREST
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
SCAN_INTERVAL = timedelta(seconds=BINARY_SENSOR_SCAN_INTERVAL_SECS)
|
||||
|
||||
BINARY_SENSORS = {
|
||||
'motion_detected': 'Motion Detected'
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
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)
|
||||
name = discovery_info[CONF_NAME]
|
||||
device = hass.data[DATA_AMCREST]['devices'][name]
|
||||
async_add_entities(
|
||||
[AmcrestBinarySensor(name, device, sensor_type)
|
||||
for sensor_type in discovery_info[CONF_BINARY_SENSORS]],
|
||||
True)
|
||||
|
||||
|
||||
class AmcrestBinarySensor(BinarySensorDevice):
|
||||
"""Binary sensor for Amcrest camera."""
|
||||
|
||||
def __init__(self, name, camera, sensor_type):
|
||||
def __init__(self, name, device, sensor_type):
|
||||
"""Initialize entity."""
|
||||
self._name = '{} {}'.format(name, BINARY_SENSORS[sensor_type])
|
||||
self._camera = camera
|
||||
self._api = device.api
|
||||
self._sensor_type = sensor_type
|
||||
self._state = None
|
||||
|
||||
@@ -62,7 +63,7 @@ class AmcrestBinarySensor(BinarySensorDevice):
|
||||
_LOGGER.debug('Pulling data from %s binary sensor', self._name)
|
||||
|
||||
try:
|
||||
self._state = self._camera.is_motion_detected
|
||||
self._state = self._api.is_motion_detected
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not update %s binary sensor due to error: %s',
|
||||
|
||||
@@ -2,18 +2,72 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import (
|
||||
Camera, SUPPORT_ON_OFF, SUPPORT_STREAM)
|
||||
Camera, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF, SUPPORT_STREAM)
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, STATE_ON, STATE_OFF)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_aiohttp_proxy_stream, async_aiohttp_proxy_web,
|
||||
async_get_clientsession)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from . import DATA_AMCREST, STREAM_SOURCE_LIST, TIMEOUT
|
||||
from .const import CAMERA_WEB_SESSION_TIMEOUT, DATA_AMCREST
|
||||
from .helpers import service_signal
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STREAM_SOURCE_LIST = [
|
||||
'snapshot',
|
||||
'mjpeg',
|
||||
'rtsp',
|
||||
]
|
||||
|
||||
_SRV_EN_REC = 'enable_recording'
|
||||
_SRV_DS_REC = 'disable_recording'
|
||||
_SRV_EN_AUD = 'enable_audio'
|
||||
_SRV_DS_AUD = 'disable_audio'
|
||||
_SRV_EN_MOT_REC = 'enable_motion_recording'
|
||||
_SRV_DS_MOT_REC = 'disable_motion_recording'
|
||||
_SRV_GOTO = 'goto_preset'
|
||||
_SRV_CBW = 'set_color_bw'
|
||||
_SRV_TOUR_ON = 'start_tour'
|
||||
_SRV_TOUR_OFF = 'stop_tour'
|
||||
|
||||
_ATTR_PRESET = 'preset'
|
||||
_ATTR_COLOR_BW = 'color_bw'
|
||||
|
||||
_CBW_COLOR = 'color'
|
||||
_CBW_AUTO = 'auto'
|
||||
_CBW_BW = 'bw'
|
||||
_CBW = [_CBW_COLOR, _CBW_AUTO, _CBW_BW]
|
||||
|
||||
_SRV_GOTO_SCHEMA = CAMERA_SERVICE_SCHEMA.extend({
|
||||
vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
})
|
||||
_SRV_CBW_SCHEMA = CAMERA_SERVICE_SCHEMA.extend({
|
||||
vol.Required(_ATTR_COLOR_BW): vol.In(_CBW),
|
||||
})
|
||||
|
||||
CAMERA_SERVICES = {
|
||||
_SRV_EN_REC: (CAMERA_SERVICE_SCHEMA, 'async_enable_recording', ()),
|
||||
_SRV_DS_REC: (CAMERA_SERVICE_SCHEMA, 'async_disable_recording', ()),
|
||||
_SRV_EN_AUD: (CAMERA_SERVICE_SCHEMA, 'async_enable_audio', ()),
|
||||
_SRV_DS_AUD: (CAMERA_SERVICE_SCHEMA, 'async_disable_audio', ()),
|
||||
_SRV_EN_MOT_REC: (
|
||||
CAMERA_SERVICE_SCHEMA, 'async_enable_motion_recording', ()),
|
||||
_SRV_DS_MOT_REC: (
|
||||
CAMERA_SERVICE_SCHEMA, 'async_disable_motion_recording', ()),
|
||||
_SRV_GOTO: (_SRV_GOTO_SCHEMA, 'async_goto_preset', (_ATTR_PRESET,)),
|
||||
_SRV_CBW: (_SRV_CBW_SCHEMA, 'async_set_color_bw', (_ATTR_COLOR_BW,)),
|
||||
_SRV_TOUR_ON: (CAMERA_SERVICE_SCHEMA, 'async_start_tour', ()),
|
||||
_SRV_TOUR_OFF: (CAMERA_SERVICE_SCHEMA, 'async_stop_tour', ()),
|
||||
}
|
||||
|
||||
_BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF}
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
@@ -21,28 +75,33 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
device_name = discovery_info[CONF_NAME]
|
||||
amcrest = hass.data[DATA_AMCREST][device_name]
|
||||
|
||||
async_add_entities([AmcrestCam(hass, amcrest)], True)
|
||||
name = discovery_info[CONF_NAME]
|
||||
device = hass.data[DATA_AMCREST]['devices'][name]
|
||||
async_add_entities([
|
||||
AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True)
|
||||
|
||||
|
||||
class AmcrestCam(Camera):
|
||||
"""An implementation of an Amcrest IP camera."""
|
||||
|
||||
def __init__(self, hass, amcrest):
|
||||
def __init__(self, name, device, ffmpeg):
|
||||
"""Initialize an Amcrest camera."""
|
||||
super(AmcrestCam, self).__init__()
|
||||
self._name = amcrest.name
|
||||
self._camera = amcrest.device
|
||||
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
|
||||
super().__init__()
|
||||
self._name = name
|
||||
self._api = device.api
|
||||
self._ffmpeg = ffmpeg
|
||||
self._ffmpeg_arguments = device.ffmpeg_arguments
|
||||
self._stream_source = device.stream_source
|
||||
self._resolution = device.resolution
|
||||
self._token = self._auth = device.authentication
|
||||
self._is_recording = False
|
||||
self._motion_detection_enabled = None
|
||||
self._model = None
|
||||
self._audio_enabled = None
|
||||
self._motion_recording_enabled = None
|
||||
self._color_bw = None
|
||||
self._snapshot_lock = asyncio.Lock()
|
||||
self._unsub_dispatcher = []
|
||||
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
@@ -56,7 +115,7 @@ class AmcrestCam(Camera):
|
||||
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)
|
||||
self._api.snapshot, self._resolution)
|
||||
return response.data
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
@@ -67,15 +126,16 @@ class AmcrestCam(Camera):
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Return an MJPEG stream."""
|
||||
# The snapshot implementation is handled by the parent class
|
||||
if self._stream_source == STREAM_SOURCE_LIST['snapshot']:
|
||||
if self._stream_source == 'snapshot':
|
||||
return await super().handle_async_mjpeg_stream(request)
|
||||
|
||||
if self._stream_source == STREAM_SOURCE_LIST['mjpeg']:
|
||||
if self._stream_source == 'mjpeg':
|
||||
# stream an MJPEG image stream directly from the camera
|
||||
websession = async_get_clientsession(self.hass)
|
||||
streaming_url = self._camera.mjpeg_url(typeno=self._resolution)
|
||||
streaming_url = self._api.mjpeg_url(typeno=self._resolution)
|
||||
stream_coro = websession.get(
|
||||
streaming_url, auth=self._token, timeout=TIMEOUT)
|
||||
streaming_url, auth=self._token,
|
||||
timeout=CAMERA_WEB_SESSION_TIMEOUT)
|
||||
|
||||
return await async_aiohttp_proxy_web(
|
||||
self.hass, request, stream_coro)
|
||||
@@ -83,7 +143,7 @@ class AmcrestCam(Camera):
|
||||
# streaming via ffmpeg
|
||||
from haffmpeg.camera import CameraMjpeg
|
||||
|
||||
streaming_url = self._camera.rtsp_url(typeno=self._resolution)
|
||||
streaming_url = self._api.rtsp_url(typeno=self._resolution)
|
||||
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
|
||||
await stream.open_camera(
|
||||
streaming_url, extra_cmd=self._ffmpeg_arguments)
|
||||
@@ -103,6 +163,19 @@ class AmcrestCam(Camera):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the Amcrest-specific camera state attributes."""
|
||||
attr = {}
|
||||
if self._audio_enabled is not None:
|
||||
attr['audio'] = _BOOL_TO_STATE.get(self._audio_enabled)
|
||||
if self._motion_recording_enabled is not None:
|
||||
attr['motion_recording'] = _BOOL_TO_STATE.get(
|
||||
self._motion_recording_enabled)
|
||||
if self._color_bw is not None:
|
||||
attr[_ATTR_COLOR_BW] = self._color_bw
|
||||
return attr
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return supported features."""
|
||||
@@ -120,15 +193,19 @@ class AmcrestCam(Camera):
|
||||
"""Return the camera brand."""
|
||||
return 'Amcrest'
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Return the camera motion detection status."""
|
||||
return self._motion_detection_enabled
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Return the camera model."""
|
||||
return self._model
|
||||
|
||||
@property
|
||||
def stream_source(self):
|
||||
async def stream_source(self):
|
||||
"""Return the source of the stream."""
|
||||
return self._camera.rtsp_url(typeno=self._resolution)
|
||||
return self._api.rtsp_url(typeno=self._resolution)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@@ -137,6 +214,21 @@ class AmcrestCam(Camera):
|
||||
|
||||
# Other Entity method overrides
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to signals and add camera to list."""
|
||||
for service, params in CAMERA_SERVICES.items():
|
||||
self._unsub_dispatcher.append(async_dispatcher_connect(
|
||||
self.hass,
|
||||
service_signal(service, self.entity_id),
|
||||
getattr(self, params[1])))
|
||||
self.hass.data[DATA_AMCREST]['cameras'].append(self.entity_id)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Remove camera from list and disconnect from signals."""
|
||||
self.hass.data[DATA_AMCREST]['cameras'].remove(self.entity_id)
|
||||
for unsub_dispatcher in self._unsub_dispatcher:
|
||||
unsub_dispatcher()
|
||||
|
||||
def update(self):
|
||||
"""Update entity status."""
|
||||
from amcrest import AmcrestError
|
||||
@@ -144,15 +236,21 @@ class AmcrestCam(Camera):
|
||||
_LOGGER.debug('Pulling data from %s camera', self.name)
|
||||
if self._model is None:
|
||||
try:
|
||||
self._model = self._camera.device_type.split('=')[-1].strip()
|
||||
self._model = self._api.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'
|
||||
self.is_streaming = self._api.video_enabled
|
||||
self._is_recording = self._api.record_mode == 'Manual'
|
||||
self._motion_detection_enabled = (
|
||||
self._api.is_motion_detector_on())
|
||||
self._audio_enabled = self._api.audio_enabled
|
||||
self._motion_recording_enabled = (
|
||||
self._api.is_record_on_motion_detection())
|
||||
self._color_bw = _CBW[self._api.day_night_color]
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not get %s camera attributes due to error: %s',
|
||||
@@ -168,14 +266,71 @@ class AmcrestCam(Camera):
|
||||
"""Turn on camera."""
|
||||
self._enable_video_stream(True)
|
||||
|
||||
# Utility methods
|
||||
def enable_motion_detection(self):
|
||||
"""Enable motion detection in the camera."""
|
||||
self._enable_motion_detection(True)
|
||||
|
||||
def disable_motion_detection(self):
|
||||
"""Disable motion detection in camera."""
|
||||
self._enable_motion_detection(False)
|
||||
|
||||
# Additional Amcrest Camera service methods
|
||||
|
||||
async def async_enable_recording(self):
|
||||
"""Call the job and enable recording."""
|
||||
await self.hass.async_add_executor_job(self._enable_recording, True)
|
||||
|
||||
async def async_disable_recording(self):
|
||||
"""Call the job and disable recording."""
|
||||
await self.hass.async_add_executor_job(self._enable_recording, False)
|
||||
|
||||
async def async_enable_audio(self):
|
||||
"""Call the job and enable audio."""
|
||||
await self.hass.async_add_executor_job(self._enable_audio, True)
|
||||
|
||||
async def async_disable_audio(self):
|
||||
"""Call the job and disable audio."""
|
||||
await self.hass.async_add_executor_job(self._enable_audio, False)
|
||||
|
||||
async def async_enable_motion_recording(self):
|
||||
"""Call the job and enable motion recording."""
|
||||
await self.hass.async_add_executor_job(self._enable_motion_recording,
|
||||
True)
|
||||
|
||||
async def async_disable_motion_recording(self):
|
||||
"""Call the job and disable motion recording."""
|
||||
await self.hass.async_add_executor_job(self._enable_motion_recording,
|
||||
False)
|
||||
|
||||
async def async_goto_preset(self, preset):
|
||||
"""Call the job and move camera to preset position."""
|
||||
await self.hass.async_add_executor_job(self._goto_preset, preset)
|
||||
|
||||
async def async_set_color_bw(self, color_bw):
|
||||
"""Call the job and set camera color mode."""
|
||||
await self.hass.async_add_executor_job(self._set_color_bw, color_bw)
|
||||
|
||||
async def async_start_tour(self):
|
||||
"""Call the job and start camera tour."""
|
||||
await self.hass.async_add_executor_job(self._start_tour, True)
|
||||
|
||||
async def async_stop_tour(self):
|
||||
"""Call the job and stop camera tour."""
|
||||
await self.hass.async_add_executor_job(self._start_tour, False)
|
||||
|
||||
# Methods to send commands to Amcrest camera and handle errors
|
||||
|
||||
def _enable_video_stream(self, enable):
|
||||
"""Enable or disable camera video stream."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
# Given the way the camera's state is determined by
|
||||
# is_streaming and is_recording, we can't leave
|
||||
# recording on if video stream is being turned off.
|
||||
if self.is_recording and not enable:
|
||||
self._enable_recording(False)
|
||||
try:
|
||||
self._camera.video_enabled = enable
|
||||
self._api.video_enabled = enable
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera video stream due to error: %s',
|
||||
@@ -183,3 +338,103 @@ class AmcrestCam(Camera):
|
||||
else:
|
||||
self.is_streaming = enable
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _enable_recording(self, enable):
|
||||
"""Turn recording on or off."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
# Given the way the camera's state is determined by
|
||||
# is_streaming and is_recording, we can't leave
|
||||
# video stream off if recording is being turned on.
|
||||
if not self.is_streaming and enable:
|
||||
self._enable_video_stream(True)
|
||||
rec_mode = {'Automatic': 0, 'Manual': 1}
|
||||
try:
|
||||
self._api.record_mode = rec_mode[
|
||||
'Manual' if enable else 'Automatic']
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera recording due to error: %s',
|
||||
'enable' if enable else 'disable', self.name, error)
|
||||
else:
|
||||
self._is_recording = enable
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _enable_motion_detection(self, enable):
|
||||
"""Enable or disable motion detection."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.motion_detection = str(enable).lower()
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera motion detection due to error: %s',
|
||||
'enable' if enable else 'disable', self.name, error)
|
||||
else:
|
||||
self._motion_detection_enabled = enable
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _enable_audio(self, enable):
|
||||
"""Enable or disable audio stream."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.audio_enabled = enable
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera audio stream due to error: %s',
|
||||
'enable' if enable else 'disable', self.name, error)
|
||||
else:
|
||||
self._audio_enabled = enable
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _enable_motion_recording(self, enable):
|
||||
"""Enable or disable motion recording."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.motion_recording = str(enable).lower()
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera motion recording due to error: %s',
|
||||
'enable' if enable else 'disable', self.name, error)
|
||||
else:
|
||||
self._motion_recording_enabled = enable
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _goto_preset(self, preset):
|
||||
"""Move camera position and zoom to preset."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.go_to_preset(
|
||||
action='start', preset_point_number=preset)
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not move %s camera to preset %i due to error: %s',
|
||||
self.name, preset, error)
|
||||
|
||||
def _set_color_bw(self, cbw):
|
||||
"""Set camera color mode."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.day_night_color = _CBW.index(cbw)
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not set %s camera color mode to %s due to error: %s',
|
||||
self.name, cbw, error)
|
||||
else:
|
||||
self._color_bw = cbw
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _start_tour(self, start):
|
||||
"""Start camera tour."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.tour(start=start)
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera tour due to error: %s',
|
||||
'start' if start else 'stop', self.name, error)
|
||||
|
||||
7
homeassistant/components/amcrest/const.py
Normal file
7
homeassistant/components/amcrest/const.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Constants for amcrest component."""
|
||||
DOMAIN = 'amcrest'
|
||||
DATA_AMCREST = DOMAIN
|
||||
|
||||
BINARY_SENSOR_SCAN_INTERVAL_SECS = 5
|
||||
CAMERA_WEB_SESSION_TIMEOUT = 10
|
||||
SENSOR_SCAN_INTERVAL_SECS = 10
|
||||
10
homeassistant/components/amcrest/helpers.py
Normal file
10
homeassistant/components/amcrest/helpers.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""Helpers for amcrest component."""
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
def service_signal(service, entity_id=None):
|
||||
"""Encode service and entity_id into signal."""
|
||||
signal = '{}_{}'.format(DOMAIN, service)
|
||||
if entity_id:
|
||||
signal += '_{}'.format(entity_id.replace('.', '_'))
|
||||
return signal
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Amcrest",
|
||||
"documentation": "https://www.home-assistant.io/components/amcrest",
|
||||
"requirements": [
|
||||
"amcrest==1.3.0"
|
||||
"amcrest==1.4.0"
|
||||
],
|
||||
"dependencies": [
|
||||
"ffmpeg"
|
||||
|
||||
@@ -5,11 +5,19 @@ import logging
|
||||
from homeassistant.const import CONF_NAME, CONF_SENSORS
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from . import DATA_AMCREST, SENSORS
|
||||
from .const import DATA_AMCREST, SENSOR_SCAN_INTERVAL_SECS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
SCAN_INTERVAL = timedelta(seconds=SENSOR_SCAN_INTERVAL_SECS)
|
||||
|
||||
# Sensor types are defined like: Name, units, icon
|
||||
SENSOR_MOTION_DETECTOR = 'motion_detector'
|
||||
SENSORS = {
|
||||
SENSOR_MOTION_DETECTOR: ['Motion Detected', None, 'mdi:run'],
|
||||
'sdcard': ['SD Used', '%', 'mdi:sd'],
|
||||
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
@@ -18,30 +26,26 @@ async def async_setup_platform(
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
device_name = discovery_info[CONF_NAME]
|
||||
sensors = discovery_info[CONF_SENSORS]
|
||||
amcrest = hass.data[DATA_AMCREST][device_name]
|
||||
|
||||
amcrest_sensors = []
|
||||
for sensor_type in sensors:
|
||||
amcrest_sensors.append(
|
||||
AmcrestSensor(amcrest.name, amcrest.device, sensor_type))
|
||||
|
||||
async_add_entities(amcrest_sensors, True)
|
||||
name = discovery_info[CONF_NAME]
|
||||
device = hass.data[DATA_AMCREST]['devices'][name]
|
||||
async_add_entities(
|
||||
[AmcrestSensor(name, device, sensor_type)
|
||||
for sensor_type in discovery_info[CONF_SENSORS]],
|
||||
True)
|
||||
|
||||
|
||||
class AmcrestSensor(Entity):
|
||||
"""A sensor implementation for Amcrest IP camera."""
|
||||
|
||||
def __init__(self, name, camera, sensor_type):
|
||||
def __init__(self, name, device, sensor_type):
|
||||
"""Initialize a sensor for Amcrest camera."""
|
||||
self._attrs = {}
|
||||
self._camera = camera
|
||||
self._name = '{} {}'.format(name, SENSORS[sensor_type][0])
|
||||
self._api = device.api
|
||||
self._sensor_type = sensor_type
|
||||
self._name = '{0}_{1}'.format(
|
||||
name, SENSORS.get(self._sensor_type)[0])
|
||||
self._icon = 'mdi:{}'.format(SENSORS.get(self._sensor_type)[2])
|
||||
self._state = None
|
||||
self._attrs = {}
|
||||
self._unit_of_measurement = SENSORS[sensor_type][1]
|
||||
self._icon = SENSORS[sensor_type][2]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -66,22 +70,30 @@ class AmcrestSensor(Entity):
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the units of measurement."""
|
||||
return SENSORS.get(self._sensor_type)[1]
|
||||
return self._unit_of_measurement
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the state."""
|
||||
_LOGGER.debug("Pulling data from %s sensor.", self._name)
|
||||
|
||||
if self._sensor_type == 'motion_detector':
|
||||
self._state = self._camera.is_motion_detected
|
||||
self._attrs['Record Mode'] = self._camera.record_mode
|
||||
self._state = self._api.is_motion_detected
|
||||
self._attrs['Record Mode'] = self._api.record_mode
|
||||
|
||||
elif self._sensor_type == 'ptz_preset':
|
||||
self._state = self._camera.ptz_presets_count
|
||||
self._state = self._api.ptz_presets_count
|
||||
|
||||
elif self._sensor_type == 'sdcard':
|
||||
sd_used = self._camera.storage_used
|
||||
sd_total = self._camera.storage_total
|
||||
self._attrs['Total'] = '{0} {1}'.format(*sd_total)
|
||||
self._attrs['Used'] = '{0} {1}'.format(*sd_used)
|
||||
self._state = self._camera.storage_used_percent
|
||||
storage = self._api.storage_all
|
||||
try:
|
||||
self._attrs['Total'] = '{:.2f} {}'.format(*storage['total'])
|
||||
except ValueError:
|
||||
self._attrs['Total'] = '{} {}'.format(*storage['total'])
|
||||
try:
|
||||
self._attrs['Used'] = '{:.2f} {}'.format(*storage['used'])
|
||||
except ValueError:
|
||||
self._attrs['Used'] = '{} {}'.format(*storage['used'])
|
||||
try:
|
||||
self._state = '{:.2f}'.format(storage['used_percent'])
|
||||
except ValueError:
|
||||
self._state = storage['used_percent']
|
||||
|
||||
75
homeassistant/components/amcrest/services.yaml
Normal file
75
homeassistant/components/amcrest/services.yaml
Normal file
@@ -0,0 +1,75 @@
|
||||
enable_recording:
|
||||
description: Enable continuous recording to camera storage.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
disable_recording:
|
||||
description: Disable continuous recording to camera storage.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
enable_audio:
|
||||
description: Enable audio stream.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
disable_audio:
|
||||
description: Disable audio stream.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
enable_motion_recording:
|
||||
description: Enable recording a clip to camera storage when motion is detected.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
disable_motion_recording:
|
||||
description: Disable recording a clip to camera storage when motion is detected.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
goto_preset:
|
||||
description: Move camera to PTZ preset.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
preset:
|
||||
description: Preset number, starting from 1.
|
||||
example: 1
|
||||
|
||||
set_color_bw:
|
||||
description: Set camera color mode.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
color_bw:
|
||||
description: Color mode, one of 'auto', 'color' or 'bw'.
|
||||
example: auto
|
||||
|
||||
start_tour:
|
||||
description: Start camera's PTZ tour function.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
stop_tour:
|
||||
description: Stop camera's PTZ tour function.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
@@ -1,13 +1,19 @@
|
||||
"""Support for toggling Amcrest IP camera settings."""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import CONF_NAME, CONF_SWITCHES, STATE_OFF, STATE_ON
|
||||
from homeassistant.const import CONF_NAME, CONF_SWITCHES
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
from . import DATA_AMCREST, SWITCHES
|
||||
from .const import DATA_AMCREST
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Switch types are defined like: Name, icon
|
||||
SWITCHES = {
|
||||
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
|
||||
'motion_recording': ['Motion Recording', 'mdi:record-rec']
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
@@ -16,67 +22,58 @@ async def async_setup_platform(
|
||||
return
|
||||
|
||||
name = discovery_info[CONF_NAME]
|
||||
switches = discovery_info[CONF_SWITCHES]
|
||||
camera = hass.data[DATA_AMCREST][name].device
|
||||
|
||||
all_switches = []
|
||||
|
||||
for setting in switches:
|
||||
all_switches.append(AmcrestSwitch(setting, camera, name))
|
||||
|
||||
async_add_entities(all_switches, True)
|
||||
device = hass.data[DATA_AMCREST]['devices'][name]
|
||||
async_add_entities(
|
||||
[AmcrestSwitch(name, device, setting)
|
||||
for setting in discovery_info[CONF_SWITCHES]],
|
||||
True)
|
||||
|
||||
|
||||
class AmcrestSwitch(ToggleEntity):
|
||||
"""Representation of an Amcrest IP camera switch."""
|
||||
|
||||
def __init__(self, setting, camera, name):
|
||||
def __init__(self, name, device, setting):
|
||||
"""Initialize the Amcrest switch."""
|
||||
self._name = '{} {}'.format(name, SWITCHES[setting][0])
|
||||
self._api = device.api
|
||||
self._setting = setting
|
||||
self._camera = camera
|
||||
self._name = '{} {}'.format(SWITCHES[setting][0], name)
|
||||
self._state = False
|
||||
self._icon = SWITCHES[setting][1]
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the switch if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the switch."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
return self._state == STATE_ON
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn setting on."""
|
||||
if self._setting == 'motion_detection':
|
||||
self._camera.motion_detection = 'true'
|
||||
self._api.motion_detection = 'true'
|
||||
elif self._setting == 'motion_recording':
|
||||
self._camera.motion_recording = 'true'
|
||||
self._api.motion_recording = 'true'
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn setting off."""
|
||||
if self._setting == 'motion_detection':
|
||||
self._camera.motion_detection = 'false'
|
||||
self._api.motion_detection = 'false'
|
||||
elif self._setting == 'motion_recording':
|
||||
self._camera.motion_recording = 'false'
|
||||
self._api.motion_recording = 'false'
|
||||
|
||||
def update(self):
|
||||
"""Update setting state."""
|
||||
_LOGGER.debug("Polling state for setting: %s ", self._name)
|
||||
|
||||
if self._setting == 'motion_detection':
|
||||
detection = self._camera.is_motion_detector_on()
|
||||
detection = self._api.is_motion_detector_on()
|
||||
elif self._setting == 'motion_recording':
|
||||
detection = self._camera.is_record_on_motion_detection()
|
||||
detection = self._api.is_record_on_motion_detection()
|
||||
|
||||
self._state = STATE_ON if detection else STATE_OFF
|
||||
self._state = detection
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
|
||||
@@ -233,7 +233,7 @@ async def async_setup(hass, config):
|
||||
|
||||
tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -90,20 +90,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
if CONF_ADB_SERVER_IP not in config:
|
||||
# Use "python-adb" (Python ADB implementation)
|
||||
adb_log = "using Python ADB implementation "
|
||||
if CONF_ADBKEY in config:
|
||||
aftv = setup(host, config[CONF_ADBKEY],
|
||||
device_class=config[CONF_DEVICE_CLASS])
|
||||
adb_log = " using adbkey='{0}'".format(config[CONF_ADBKEY])
|
||||
adb_log += "with adbkey='{0}'".format(config[CONF_ADBKEY])
|
||||
|
||||
else:
|
||||
aftv = setup(host, device_class=config[CONF_DEVICE_CLASS])
|
||||
adb_log = ""
|
||||
adb_log += "without adbkey authentication"
|
||||
else:
|
||||
# Use "pure-python-adb" (communicate with ADB server)
|
||||
aftv = setup(host, adb_server_ip=config[CONF_ADB_SERVER_IP],
|
||||
adb_server_port=config[CONF_ADB_SERVER_PORT],
|
||||
device_class=config[CONF_DEVICE_CLASS])
|
||||
adb_log = " using ADB server at {0}:{1}".format(
|
||||
adb_log = "using ADB server at {0}:{1}".format(
|
||||
config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT])
|
||||
|
||||
if not aftv.available:
|
||||
@@ -117,7 +118,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
else:
|
||||
device_name = 'Android TV / Fire TV device'
|
||||
|
||||
_LOGGER.warning("Could not connect to %s at %s%s",
|
||||
_LOGGER.warning("Could not connect to %s at %s %s",
|
||||
device_name, host, adb_log)
|
||||
raise PlatformNotReady
|
||||
|
||||
@@ -156,10 +157,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
for target_device in target_devices:
|
||||
output = target_device.adb_command(cmd)
|
||||
|
||||
# log the output if there is any
|
||||
if output and (not isinstance(output, str) or output.strip()):
|
||||
# log the output, if there is any
|
||||
if output:
|
||||
_LOGGER.info("Output of command '%s' from '%s': %s",
|
||||
cmd, target_device.entity_id, repr(output))
|
||||
cmd, target_device.entity_id, output)
|
||||
|
||||
hass.services.register(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND,
|
||||
service_adb_command,
|
||||
@@ -224,6 +225,7 @@ class ADBDevice(MediaPlayerDevice):
|
||||
self.exceptions = (ConnectionResetError, RuntimeError)
|
||||
|
||||
# Property attributes
|
||||
self._adb_response = None
|
||||
self._available = self.aftv.available
|
||||
self._current_app = None
|
||||
self._state = None
|
||||
@@ -243,6 +245,11 @@ class ADBDevice(MediaPlayerDevice):
|
||||
"""Return whether or not the ADB connection is valid."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Provide the last ADB command's response as an attribute."""
|
||||
return {'adb_response': self._adb_response}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the device name."""
|
||||
@@ -304,12 +311,24 @@ class ADBDevice(MediaPlayerDevice):
|
||||
"""Send an ADB command to an Android TV / Fire TV device."""
|
||||
key = self._keys.get(cmd)
|
||||
if key:
|
||||
return self.aftv.adb_shell('input keyevent {}'.format(key))
|
||||
self.aftv.adb_shell('input keyevent {}'.format(key))
|
||||
self._adb_response = None
|
||||
self.schedule_update_ha_state()
|
||||
return
|
||||
|
||||
if cmd == 'GET_PROPERTIES':
|
||||
return self.aftv.get_properties_dict()
|
||||
self._adb_response = str(self.aftv.get_properties_dict())
|
||||
self.schedule_update_ha_state()
|
||||
return self._adb_response
|
||||
|
||||
return self.aftv.adb_shell(cmd)
|
||||
response = self.aftv.adb_shell(cmd)
|
||||
if isinstance(response, str) and response.strip():
|
||||
self._adb_response = response.strip()
|
||||
else:
|
||||
self._adb_response = None
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
return self._adb_response
|
||||
|
||||
|
||||
class AndroidTVDevice(ADBDevice):
|
||||
|
||||
@@ -47,7 +47,7 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
hass.async_create_task(device.async_update_ha_state())
|
||||
|
||||
avr = await anthemav.Connection.create(
|
||||
host=host, port=port, loop=hass.loop,
|
||||
host=host, port=port,
|
||||
update_callback=async_anthemav_update_callback)
|
||||
|
||||
device = AnthemAVR(avr, name)
|
||||
|
||||
@@ -82,7 +82,7 @@ class APIEventStream(HomeAssistantView):
|
||||
raise Unauthorized()
|
||||
hass = request.app['hass']
|
||||
stop_obj = object()
|
||||
to_write = asyncio.Queue(loop=hass.loop)
|
||||
to_write = asyncio.Queue()
|
||||
|
||||
restrict = request.query.get('restrict')
|
||||
if restrict:
|
||||
@@ -119,8 +119,7 @@ class APIEventStream(HomeAssistantView):
|
||||
|
||||
while True:
|
||||
try:
|
||||
with async_timeout.timeout(STREAM_PING_INTERVAL,
|
||||
loop=hass.loop):
|
||||
with async_timeout.timeout(STREAM_PING_INTERVAL):
|
||||
payload = await to_write.get()
|
||||
|
||||
if payload is stop_obj:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""APNS Notification platform."""
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -149,7 +148,8 @@ class ApnsNotificationService(BaseNotificationService):
|
||||
self.devices = {}
|
||||
self.device_states = {}
|
||||
self.topic = topic
|
||||
if os.path.isfile(self.yaml_path):
|
||||
|
||||
try:
|
||||
self.devices = {
|
||||
str(key): ApnsDevice(
|
||||
str(key),
|
||||
@@ -160,6 +160,8 @@ class ApnsNotificationService(BaseNotificationService):
|
||||
for (key, value) in
|
||||
load_yaml_config_file(self.yaml_path).items()
|
||||
}
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
tracking_ids = [
|
||||
device.full_tracking_device_id
|
||||
|
||||
@@ -167,7 +167,7 @@ async def async_setup(hass, config):
|
||||
|
||||
tasks = [_setup_atv(hass, config, conf) for conf in config.get(DOMAIN, [])]
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SCAN, async_service_handler,
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "C\u00f3digo inv\u00e1lido, por favor int\u00e9ntalo de nuevo. Si recibes este error de forma consistente, por favor aseg\u00farate de que el reloj de tu Home Assistant es correcto."
|
||||
"invalid_code": "C\u00f3digo inv\u00e1lido, int\u00e9ntalo de nuevo. Si recibes este error de forma consistente, aseg\u00farate de que el reloj de tu sistema Home Assistant es correcto."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Para activar la autenticaci\u00f3n de dos factores utilizando contrase\u00f1as de un solo uso basadas en el tiempo, escanea el c\u00f3digo QR con tu aplicaci\u00f3n de autenticaci\u00f3n. Si no tienes una, te recomendamos [Autenticador de Google] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \nDespu\u00e9s de escanear el c\u00f3digo, introduce el c\u00f3digo de seis d\u00edgitos de tu aplicaci\u00f3n para verificar la configuraci\u00f3n. Si tienes problemas para escanear el c\u00f3digo QR, realiza una configuraci\u00f3n manual con el c\u00f3digo ** ` {code} ` **.",
|
||||
"description": "Para activar la autenticaci\u00f3n de dos factores utilizando contrase\u00f1as de un solo uso basadas en el tiempo, escanea el c\u00f3digo QR con tu aplicaci\u00f3n de autenticaci\u00f3n. Si no tienes una, te recomendamos el [Autenticador de Google](https://support.google.com/accounts/answer/1066447) o [Authy](https://authy.com/). \n\n {qr_code} \n \nDespu\u00e9s de escanear el c\u00f3digo, introduce el c\u00f3digo de seis d\u00edgitos de tu aplicaci\u00f3n para verificar la configuraci\u00f3n. Si tienes problemas para escanear el c\u00f3digo QR, realiza una configuraci\u00f3n manual con el c\u00f3digo **`{code}`**.",
|
||||
"title": "Configure la autenticaci\u00f3n de dos factores utilizando TOTP"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@ from html.parser import HTMLParser
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
import aiohttp
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
|
||||
from homeassistant.util.network import is_local
|
||||
|
||||
@@ -81,8 +80,22 @@ async def fetch_redirect_uris(hass, url):
|
||||
if chunks == 10:
|
||||
break
|
||||
|
||||
except (asyncio.TimeoutError, ClientError) as ex:
|
||||
_LOGGER.error("Error while looking up redirect_uri %s: %s", url, ex)
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error("Timeout while looking up redirect_uri %s", url)
|
||||
pass
|
||||
except aiohttp.client_exceptions.ClientSSLError:
|
||||
_LOGGER.error("SSL error while looking up redirect_uri %s", url)
|
||||
pass
|
||||
except aiohttp.client_exceptions.ClientOSError as ex:
|
||||
_LOGGER.error("OS error while looking up redirect_uri %s: %s", url,
|
||||
ex.strerror)
|
||||
pass
|
||||
except aiohttp.client_exceptions.ClientConnectionError:
|
||||
_LOGGER.error(("Low level connection error while looking up "
|
||||
"redirect_uri %s"), url)
|
||||
pass
|
||||
except aiohttp.client_exceptions.ClientError:
|
||||
_LOGGER.error("Unknown error while looking up redirect_uri %s", url)
|
||||
pass
|
||||
|
||||
# Authorization endpoints verifying that a redirect_uri is allowed for use
|
||||
|
||||
@@ -97,8 +97,7 @@ class AuthProvidersView(HomeAssistantView):
|
||||
async def get(self, request):
|
||||
"""Get available auth providers."""
|
||||
hass = request.app['hass']
|
||||
|
||||
if not hass.components.onboarding.async_is_onboarded():
|
||||
if not hass.components.onboarding.async_is_user_onboarded():
|
||||
return self.json_message(
|
||||
message='Onboarding not finished',
|
||||
status_code=400,
|
||||
|
||||
@@ -124,7 +124,7 @@ async def async_setup(hass, config):
|
||||
context=service_call.context))
|
||||
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
async def turn_onoff_service_handler(service_call):
|
||||
"""Handle automation turn on/off service calls."""
|
||||
@@ -134,7 +134,7 @@ async def async_setup(hass, config):
|
||||
tasks.append(getattr(entity, method)())
|
||||
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
async def toggle_service_handler(service_call):
|
||||
"""Handle automation toggle service calls."""
|
||||
@@ -146,7 +146,7 @@ async def async_setup(hass, config):
|
||||
tasks.append(entity.async_turn_on())
|
||||
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
async def reload_service_handler(service_call):
|
||||
"""Remove all automations and load new ones from config."""
|
||||
|
||||
@@ -31,6 +31,6 @@ async def async_trigger(hass, config, action, automation_info):
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
},
|
||||
}, context=to_s.context))
|
||||
}, context=(to_s.context if to_s else None)))
|
||||
|
||||
return async_track_template(hass, value_template, template_listener)
|
||||
|
||||
@@ -166,14 +166,14 @@ async def _validate_aws_credentials(hass, credential):
|
||||
profile = aws_config.get(CONF_PROFILE_NAME)
|
||||
|
||||
if profile is not None:
|
||||
session = aiobotocore.AioSession(profile=profile, loop=hass.loop)
|
||||
session = aiobotocore.AioSession(profile=profile)
|
||||
del aws_config[CONF_PROFILE_NAME]
|
||||
if CONF_ACCESS_KEY_ID in aws_config:
|
||||
del aws_config[CONF_ACCESS_KEY_ID]
|
||||
if CONF_SECRET_ACCESS_KEY in aws_config:
|
||||
del aws_config[CONF_SECRET_ACCESS_KEY]
|
||||
else:
|
||||
session = aiobotocore.AioSession(loop=hass.loop)
|
||||
session = aiobotocore.AioSession()
|
||||
|
||||
if credential[CONF_VALIDATE]:
|
||||
async with session.create_client("iam", **aws_config) as client:
|
||||
|
||||
@@ -94,10 +94,10 @@ async def async_get_service(hass, config, discovery_info=None):
|
||||
if session is None:
|
||||
profile = aws_config.get(CONF_PROFILE_NAME)
|
||||
if profile is not None:
|
||||
session = aiobotocore.AioSession(profile=profile, loop=hass.loop)
|
||||
session = aiobotocore.AioSession(profile=profile)
|
||||
del aws_config[CONF_PROFILE_NAME]
|
||||
else:
|
||||
session = aiobotocore.AioSession(loop=hass.loop)
|
||||
session = aiobotocore.AioSession()
|
||||
|
||||
aws_config[CONF_REGION] = region_name
|
||||
|
||||
|
||||
18
homeassistant/components/axis/.translations/nl.json
Normal file
18
homeassistant/components/axis/.translations/nl.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"device_unavailable": "Apparaat is niet beschikbaar",
|
||||
"faulty_credentials": "Ongeldige gebruikersreferenties"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Wachtwoord",
|
||||
"port": "Poort",
|
||||
"username": "Gebruikersnaam"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Enheten \u00e4r redan konfigurerad",
|
||||
"bad_config_file": "Felaktig data fr\u00e5n config fil"
|
||||
"bad_config_file": "Felaktig data fr\u00e5n config fil",
|
||||
"link_local_address": "Link local addresses are not supported"
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "Enheten \u00e4r redan konfigurerad",
|
||||
@@ -17,7 +18,7 @@
|
||||
"port": "Port",
|
||||
"username": "Anv\u00e4ndarnamn"
|
||||
},
|
||||
"title": "Konfigurera Axis enhet"
|
||||
"title": "Konfigurera Axis-enhet"
|
||||
}
|
||||
},
|
||||
"title": "Axis enhet"
|
||||
|
||||
86
homeassistant/components/axis/axis_base.py
Normal file
86
homeassistant/components/axis/axis_base.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Base classes for Axis entities."""
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN as AXIS_DOMAIN
|
||||
|
||||
|
||||
class AxisEntityBase(Entity):
|
||||
"""Base common to all Axis entities."""
|
||||
|
||||
def __init__(self, device):
|
||||
"""Initialize the Axis event."""
|
||||
self.device = device
|
||||
self.unsub_dispatcher = []
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe device events."""
|
||||
self.unsub_dispatcher.append(async_dispatcher_connect(
|
||||
self.hass, self.device.event_reachable, self.update_callback))
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Unsubscribe device events when removed."""
|
||||
for unsub_dispatcher in self.unsub_dispatcher:
|
||||
unsub_dispatcher()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if device is available."""
|
||||
return self.device.available
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return a device description for device registry."""
|
||||
return {
|
||||
'identifiers': {(AXIS_DOMAIN, self.device.serial)}
|
||||
}
|
||||
|
||||
@callback
|
||||
def update_callback(self, no_delay=None):
|
||||
"""Update the entities state."""
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
|
||||
class AxisEventBase(AxisEntityBase):
|
||||
"""Base common to all Axis entities from event stream."""
|
||||
|
||||
def __init__(self, event, device):
|
||||
"""Initialize the Axis event."""
|
||||
super().__init__(device)
|
||||
self.event = event
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe sensors events."""
|
||||
self.event.register_callback(self.update_callback)
|
||||
|
||||
await super().async_added_to_hass()
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect device object when removed."""
|
||||
self.event.remove_callback(self.update_callback)
|
||||
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the event."""
|
||||
return self.event.CLASS
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the event."""
|
||||
return '{} {} {}'.format(
|
||||
self.device.name, self.event.TYPE, self.event.id)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique identifier for this device."""
|
||||
return '{}-{}-{}'.format(
|
||||
self.device.serial, self.event.topic, self.event.id)
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from axis.event_stream import CLASS_INPUT, CLASS_OUTPUT
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import CONF_MAC, CONF_TRIGGER_TIME
|
||||
from homeassistant.core import callback
|
||||
@@ -9,7 +11,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .const import DOMAIN as AXIS_DOMAIN, LOGGER
|
||||
from .axis_base import AxisEventBase
|
||||
from .const import DOMAIN as AXIS_DOMAIN
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
@@ -21,32 +24,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
def async_add_sensor(event_id):
|
||||
"""Add binary sensor from Axis device."""
|
||||
event = device.api.event.events[event_id]
|
||||
async_add_entities([AxisBinarySensor(event, device)], True)
|
||||
|
||||
if event.CLASS != CLASS_OUTPUT:
|
||||
async_add_entities([AxisBinarySensor(event, device)], True)
|
||||
|
||||
device.listeners.append(async_dispatcher_connect(
|
||||
hass, device.event_new_sensor, async_add_sensor))
|
||||
|
||||
|
||||
class AxisBinarySensor(BinarySensorDevice):
|
||||
class AxisBinarySensor(AxisEventBase, BinarySensorDevice):
|
||||
"""Representation of a binary Axis event."""
|
||||
|
||||
def __init__(self, event, device):
|
||||
"""Initialize the Axis binary sensor."""
|
||||
self.event = event
|
||||
self.device = device
|
||||
super().__init__(event, device)
|
||||
self.remove_timer = None
|
||||
self.unsub_dispatcher = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe sensors events."""
|
||||
self.event.register_callback(self.update_callback)
|
||||
self.unsub_dispatcher = async_dispatcher_connect(
|
||||
self.hass, self.device.event_reachable, self.update_callback)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect device object when removed."""
|
||||
self.event.remove_callback(self.update_callback)
|
||||
self.unsub_dispatcher()
|
||||
|
||||
@callback
|
||||
def update_callback(self, no_delay=False):
|
||||
@@ -67,7 +59,6 @@ class AxisBinarySensor(BinarySensorDevice):
|
||||
@callback
|
||||
def _delay_update(now):
|
||||
"""Timer callback for sensor update."""
|
||||
LOGGER.debug("%s called delayed (%s sec) update", self.name, delay)
|
||||
self.async_schedule_update_ha_state()
|
||||
self.remove_timer = None
|
||||
|
||||
@@ -83,32 +74,10 @@ class AxisBinarySensor(BinarySensorDevice):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the event."""
|
||||
return '{} {} {}'.format(
|
||||
self.device.name, self.event.TYPE, self.event.id)
|
||||
if self.event.CLASS == CLASS_INPUT and self.event.id and \
|
||||
self.device.api.vapix.ports[self.event.id].name:
|
||||
return '{} {}'.format(
|
||||
self.device.name,
|
||||
self.device.api.vapix.ports[self.event.id].name)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the event."""
|
||||
return self.event.CLASS
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique identifier for this device."""
|
||||
return '{}-{}-{}'.format(
|
||||
self.device.serial, self.event.topic, self.event.id)
|
||||
|
||||
def available(self):
|
||||
"""Return True if device is available."""
|
||||
return self.device.available
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return a device description for device registry."""
|
||||
return {
|
||||
'identifiers': {(AXIS_DOMAIN, self.device.serial)}
|
||||
}
|
||||
return super().name
|
||||
|
||||
@@ -6,9 +6,9 @@ from homeassistant.components.mjpeg.camera import (
|
||||
from homeassistant.const import (
|
||||
CONF_AUTHENTICATION, CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME,
|
||||
CONF_PASSWORD, CONF_PORT, CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .axis_base import AxisEntityBase
|
||||
from .const import DOMAIN as AXIS_DOMAIN
|
||||
|
||||
AXIS_IMAGE = 'http://{}:{}/axis-cgi/jpg/image.cgi'
|
||||
@@ -38,65 +38,40 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
async_add_entities([AxisCamera(config, device)])
|
||||
|
||||
|
||||
class AxisCamera(MjpegCamera):
|
||||
class AxisCamera(AxisEntityBase, MjpegCamera):
|
||||
"""Representation of a Axis camera."""
|
||||
|
||||
def __init__(self, config, device):
|
||||
"""Initialize Axis Communications camera component."""
|
||||
super().__init__(config)
|
||||
self.device_config = config
|
||||
self.device = device
|
||||
self.port = device.config_entry.data[CONF_DEVICE][CONF_PORT]
|
||||
self.unsub_dispatcher = []
|
||||
AxisEntityBase.__init__(self, device)
|
||||
MjpegCamera.__init__(self, config)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe camera events."""
|
||||
self.unsub_dispatcher.append(async_dispatcher_connect(
|
||||
self.hass, self.device.event_new_address, self._new_address))
|
||||
self.unsub_dispatcher.append(async_dispatcher_connect(
|
||||
self.hass, self.device.event_reachable, self.update_callback))
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect device object when removed."""
|
||||
for unsub_dispatcher in self.unsub_dispatcher:
|
||||
unsub_dispatcher()
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return supported features."""
|
||||
return SUPPORT_STREAM
|
||||
|
||||
@property
|
||||
def stream_source(self):
|
||||
async def stream_source(self):
|
||||
"""Return the stream source."""
|
||||
return AXIS_STREAM.format(
|
||||
self.device.config_entry.data[CONF_DEVICE][CONF_USERNAME],
|
||||
self.device.config_entry.data[CONF_DEVICE][CONF_PASSWORD],
|
||||
self.device.host)
|
||||
|
||||
@callback
|
||||
def update_callback(self, no_delay=None):
|
||||
"""Update the cameras state."""
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if device is available."""
|
||||
return self.device.available
|
||||
|
||||
def _new_address(self):
|
||||
"""Set new device address for video stream."""
|
||||
self._mjpeg_url = AXIS_VIDEO.format(self.device.host, self.port)
|
||||
self._still_image_url = AXIS_IMAGE.format(self.device.host, self.port)
|
||||
port = self.device.config_entry.data[CONF_DEVICE][CONF_PORT]
|
||||
self._mjpeg_url = AXIS_VIDEO.format(self.device.host, port)
|
||||
self._still_image_url = AXIS_IMAGE.format(self.device.host, port)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique identifier for this device."""
|
||||
return '{}-camera'.format(self.device.serial)
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return a device description for device registry."""
|
||||
return {
|
||||
'identifiers': {(AXIS_DOMAIN, self.device.serial)}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ class AxisFlowHandler(config_entries.ConfigFlow):
|
||||
entry.data[CONF_DEVICE][CONF_HOST] = host
|
||||
self.hass.config_entries.async_update_entry(entry)
|
||||
|
||||
async def async_step_discovery(self, discovery_info):
|
||||
async def async_step_zeroconf(self, discovery_info):
|
||||
"""Prepare configuration for a discovered Axis device.
|
||||
|
||||
This flow is triggered by the discovery component.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Constants for the Axis component."""
|
||||
import logging
|
||||
|
||||
LOGGER = logging.getLogger('homeassistant.components.axis')
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = 'axis'
|
||||
|
||||
|
||||
@@ -83,19 +83,23 @@ class AxisNetworkDevice:
|
||||
self.product_type = self.api.vapix.params.prodtype
|
||||
|
||||
if self.config_entry.options[CONF_CAMERA]:
|
||||
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_forward_entry_setup(
|
||||
self.config_entry, 'camera'))
|
||||
|
||||
if self.config_entry.options[CONF_EVENTS]:
|
||||
task = self.hass.async_create_task(
|
||||
self.hass.config_entries.async_forward_entry_setup(
|
||||
self.config_entry, 'binary_sensor'))
|
||||
|
||||
self.api.stream.connection_status_callback = \
|
||||
self.async_connection_status_callback
|
||||
self.api.enable_events(event_callback=self.async_event_callback)
|
||||
task.add_done_callback(self.start)
|
||||
|
||||
platform_tasks = [
|
||||
self.hass.config_entries.async_forward_entry_setup(
|
||||
self.config_entry, platform)
|
||||
for platform in ['binary_sensor', 'switch']
|
||||
]
|
||||
self.hass.async_create_task(self.start(platform_tasks))
|
||||
|
||||
self.config_entry.add_update_listener(self.async_new_address_callback)
|
||||
|
||||
@@ -145,9 +149,9 @@ class AxisNetworkDevice:
|
||||
if action == 'add':
|
||||
async_dispatcher_send(self.hass, self.event_new_sensor, event_id)
|
||||
|
||||
@callback
|
||||
def start(self, fut):
|
||||
"""Start the event stream."""
|
||||
async def start(self, platform_tasks):
|
||||
"""Start the event stream when all platforms are loaded."""
|
||||
await asyncio.gather(*platform_tasks)
|
||||
self.api.start()
|
||||
|
||||
@callback
|
||||
@@ -157,15 +161,22 @@ class AxisNetworkDevice:
|
||||
|
||||
async def async_reset(self):
|
||||
"""Reset this device to default state."""
|
||||
self.api.stop()
|
||||
platform_tasks = []
|
||||
|
||||
if self.config_entry.options[CONF_CAMERA]:
|
||||
await self.hass.config_entries.async_forward_entry_unload(
|
||||
self.config_entry, 'camera')
|
||||
platform_tasks.append(
|
||||
self.hass.config_entries.async_forward_entry_unload(
|
||||
self.config_entry, 'camera'))
|
||||
|
||||
if self.config_entry.options[CONF_EVENTS]:
|
||||
await self.hass.config_entries.async_forward_entry_unload(
|
||||
self.config_entry, 'binary_sensor')
|
||||
self.api.stop()
|
||||
platform_tasks += [
|
||||
self.hass.config_entries.async_forward_entry_unload(
|
||||
self.config_entry, platform)
|
||||
for platform in ['binary_sensor', 'switch']
|
||||
]
|
||||
|
||||
await asyncio.gather(*platform_tasks)
|
||||
|
||||
for unsub_dispatcher in self.listeners:
|
||||
unsub_dispatcher()
|
||||
@@ -185,13 +196,22 @@ async def get_device(hass, config):
|
||||
port=config[CONF_PORT], web_proto='http')
|
||||
|
||||
device.vapix.initialize_params(preload_data=False)
|
||||
device.vapix.initialize_ports()
|
||||
|
||||
try:
|
||||
with async_timeout.timeout(15):
|
||||
await hass.async_add_executor_job(
|
||||
device.vapix.params.update_brand)
|
||||
await hass.async_add_executor_job(
|
||||
device.vapix.params.update_properties)
|
||||
|
||||
await asyncio.gather(
|
||||
hass.async_add_executor_job(
|
||||
device.vapix.params.update_brand),
|
||||
|
||||
hass.async_add_executor_job(
|
||||
device.vapix.params.update_properties),
|
||||
|
||||
hass.async_add_executor_job(
|
||||
device.vapix.ports.update)
|
||||
)
|
||||
|
||||
return device
|
||||
|
||||
except axis.Unauthorized:
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"domain": "axis",
|
||||
"name": "Axis",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/axis",
|
||||
"requirements": ["axis==22"],
|
||||
"requirements": ["axis==23"],
|
||||
"dependencies": [],
|
||||
"zeroconf": ["_axis-video._tcp.local."],
|
||||
"codeowners": ["@kane610"]
|
||||
}
|
||||
|
||||
59
homeassistant/components/axis/switch.py
Normal file
59
homeassistant/components/axis/switch.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Support for Axis switches."""
|
||||
|
||||
from axis.event_stream import CLASS_OUTPUT
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.const import CONF_MAC
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .axis_base import AxisEventBase
|
||||
from .const import DOMAIN as AXIS_DOMAIN
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up a Axis switch."""
|
||||
serial_number = config_entry.data[CONF_MAC]
|
||||
device = hass.data[AXIS_DOMAIN][serial_number]
|
||||
|
||||
@callback
|
||||
def async_add_switch(event_id):
|
||||
"""Add switch from Axis device."""
|
||||
event = device.api.event.events[event_id]
|
||||
|
||||
if event.CLASS == CLASS_OUTPUT:
|
||||
async_add_entities([AxisSwitch(event, device)], True)
|
||||
|
||||
device.listeners.append(async_dispatcher_connect(
|
||||
hass, device.event_new_sensor, async_add_switch))
|
||||
|
||||
|
||||
class AxisSwitch(AxisEventBase, SwitchDevice):
|
||||
"""Representation of a Axis switch."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if event is active."""
|
||||
return self.event.is_tripped
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn on switch."""
|
||||
action = '/'
|
||||
await self.hass.async_add_executor_job(
|
||||
self.device.api.vapix.ports[self.event.id].action, action)
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn off switch."""
|
||||
action = '\\'
|
||||
await self.hass.async_add_executor_job(
|
||||
self.device.api.vapix.ports[self.event.id].action, action)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the event."""
|
||||
if self.event.id and self.device.api.vapix.ports[self.event.id].name:
|
||||
return '{} {}'.format(
|
||||
self.device.name,
|
||||
self.device.api.vapix.ports[self.event.id].name)
|
||||
|
||||
return super().name
|
||||
80
homeassistant/components/azure_event_hub/__init__.py
Normal file
80
homeassistant/components/azure_event_hub/__init__.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""Support for Azure Event Hubs."""
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
import voluptuous as vol
|
||||
from azure.eventhub import EventData, EventHubClientAsync
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
|
||||
from homeassistant.helpers.json import JSONEncoder
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'azure_event_hub'
|
||||
|
||||
CONF_EVENT_HUB_NAMESPACE = 'event_hub_namespace'
|
||||
CONF_EVENT_HUB_INSTANCE_NAME = 'event_hub_instance_name'
|
||||
CONF_EVENT_HUB_SAS_POLICY = 'event_hub_sas_policy'
|
||||
CONF_EVENT_HUB_SAS_KEY = 'event_hub_sas_key'
|
||||
CONF_FILTER = 'filter'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_EVENT_HUB_NAMESPACE): cv.string,
|
||||
vol.Required(CONF_EVENT_HUB_INSTANCE_NAME): cv.string,
|
||||
vol.Required(CONF_EVENT_HUB_SAS_POLICY): cv.string,
|
||||
vol.Required(CONF_EVENT_HUB_SAS_KEY): cv.string,
|
||||
vol.Required(CONF_FILTER): FILTER_SCHEMA,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
|
||||
"""Activate Azure EH component."""
|
||||
config = yaml_config[DOMAIN]
|
||||
|
||||
event_hub_address = "amqps://{}.servicebus.windows.net/{}".format(
|
||||
config[CONF_EVENT_HUB_NAMESPACE],
|
||||
config[CONF_EVENT_HUB_INSTANCE_NAME])
|
||||
entities_filter = config[CONF_FILTER]
|
||||
|
||||
client = EventHubClientAsync(
|
||||
event_hub_address,
|
||||
debug=True,
|
||||
username=config[CONF_EVENT_HUB_SAS_POLICY],
|
||||
password=config[CONF_EVENT_HUB_SAS_KEY])
|
||||
async_sender = client.add_async_sender()
|
||||
await client.run_async()
|
||||
|
||||
encoder = JSONEncoder()
|
||||
|
||||
async def async_send_to_event_hub(event: Event):
|
||||
"""Send states to Event Hub."""
|
||||
state = event.data.get('new_state')
|
||||
if (state is None
|
||||
or state.state in (STATE_UNKNOWN, '', STATE_UNAVAILABLE)
|
||||
or not entities_filter(state.entity_id)):
|
||||
return
|
||||
|
||||
event_data = EventData(
|
||||
json.dumps(
|
||||
obj=state.as_dict(),
|
||||
default=encoder.encode
|
||||
).encode('utf-8')
|
||||
)
|
||||
await async_sender.send(event_data)
|
||||
|
||||
async def async_shutdown(event: Event):
|
||||
"""Shut down the client."""
|
||||
await client.stop_async()
|
||||
|
||||
hass.bus.async_listen(EVENT_STATE_CHANGED, async_send_to_event_hub)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_shutdown)
|
||||
|
||||
return True
|
||||
8
homeassistant/components/azure_event_hub/manifest.json
Normal file
8
homeassistant/components/azure_event_hub/manifest.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"domain": "azure_event_hub",
|
||||
"name": "Azure Event Hub",
|
||||
"documentation": "https://www.home-assistant.io/components/azure_event_hub",
|
||||
"requirements": ["azure-eventhub==1.3.1"],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@eavanvalkenburg"]
|
||||
}
|
||||
1
homeassistant/components/bizkaibus/__init__.py
Normal file
1
homeassistant/components/bizkaibus/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""The Bizkaibus bus tracker component."""
|
||||
8
homeassistant/components/bizkaibus/manifest.json
Normal file
8
homeassistant/components/bizkaibus/manifest.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"domain": "bizkaibus",
|
||||
"name": "Bizkaibus",
|
||||
"documentation": "https://www.home-assistant.io/components/bizkaibus",
|
||||
"dependencies": [],
|
||||
"codeowners": ["@UgaitzEtxebarria"],
|
||||
"requirements": ["bizkaibus==0.1.1"]
|
||||
}
|
||||
88
homeassistant/components/bizkaibus/sensor.py
Executable file
88
homeassistant/components/bizkaibus/sensor.py
Executable file
@@ -0,0 +1,88 @@
|
||||
"""Support for Bizkaibus, Biscay (Basque Country, Spain) Bus service."""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
from bizkaibus.bizkaibus import BizkaibusData
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_DUE_IN = 'Due in'
|
||||
|
||||
CONF_STOP_ID = 'stopid'
|
||||
CONF_ROUTE = 'route'
|
||||
|
||||
DEFAULT_NAME = 'Next bus'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_STOP_ID): cv.string,
|
||||
vol.Required(CONF_ROUTE): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Bizkaibus public transport sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
stop = config[CONF_STOP_ID]
|
||||
route = config[CONF_ROUTE]
|
||||
|
||||
data = Bizkaibus(stop, route)
|
||||
add_entities([BizkaibusSensor(data, stop, route, name)], True)
|
||||
|
||||
|
||||
class BizkaibusSensor(Entity):
|
||||
"""The class for handling the data."""
|
||||
|
||||
def __init__(self, data, stop, route, name):
|
||||
"""Initialize the sensor."""
|
||||
self.data = data
|
||||
self.stop = stop
|
||||
self.route = route
|
||||
self._name = name
|
||||
self._state = None
|
||||
|
||||
@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 the sensor."""
|
||||
return 'minutes'
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from the webservice."""
|
||||
self.data.update()
|
||||
try:
|
||||
self._state = self.data.info[0][ATTR_DUE_IN]
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
|
||||
class Bizkaibus:
|
||||
"""The class for handling the data retrieval."""
|
||||
|
||||
def __init__(self, stop, route):
|
||||
"""Initialize the data object."""
|
||||
self.stop = stop
|
||||
self.route = route
|
||||
self.info = None
|
||||
|
||||
def update(self):
|
||||
"""Retrieve the information from API."""
|
||||
bridge = BizkaibusData(self.stop, self.route)
|
||||
bridge.getNextBus()
|
||||
self.info = bridge.info
|
||||
@@ -8,7 +8,7 @@ from homeassistant.helpers import (
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_SCAN_INTERVAL,
|
||||
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
|
||||
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
|
||||
CONF_MONITORED_CONDITIONS, CONF_MODE, CONF_OFFSET, TEMP_FAHRENHEIT)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,7 +41,7 @@ BINARY_SENSORS = {
|
||||
|
||||
SENSORS = {
|
||||
TYPE_TEMPERATURE: ['Temperature', TEMP_FAHRENHEIT, 'mdi:thermometer'],
|
||||
TYPE_BATTERY: ['Battery', '%', 'mdi:battery-80'],
|
||||
TYPE_BATTERY: ['Battery', '', 'mdi:battery-80'],
|
||||
TYPE_WIFI_STRENGTH: ['Wifi Signal', 'dBm', 'mdi:wifi-strength-2'],
|
||||
}
|
||||
|
||||
@@ -75,6 +75,8 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_BINARY_SENSORS, default={}):
|
||||
BINARY_SENSOR_SCHEMA,
|
||||
vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
|
||||
vol.Optional(CONF_OFFSET, default=1): int,
|
||||
vol.Optional(CONF_MODE, default=''): cv.string,
|
||||
})
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA)
|
||||
@@ -87,8 +89,12 @@ def setup(hass, config):
|
||||
username = conf[CONF_USERNAME]
|
||||
password = conf[CONF_PASSWORD]
|
||||
scan_interval = conf[CONF_SCAN_INTERVAL]
|
||||
is_legacy = bool(conf[CONF_MODE] == 'legacy')
|
||||
motion_interval = conf[CONF_OFFSET]
|
||||
hass.data[BLINK_DATA] = blinkpy.Blink(username=username,
|
||||
password=password)
|
||||
password=password,
|
||||
motion_interval=motion_interval,
|
||||
legacy_subdomain=is_legacy)
|
||||
hass.data[BLINK_DATA].refresh_rate = scan_interval.total_seconds()
|
||||
hass.data[BLINK_DATA].start()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Blink",
|
||||
"documentation": "https://www.home-assistant.io/components/blink",
|
||||
"requirements": [
|
||||
"blinkpy==0.13.1"
|
||||
"blinkpy==0.14.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Bluesound",
|
||||
"documentation": "https://www.home-assistant.io/components/bluesound",
|
||||
"requirements": [
|
||||
"xmltodict==0.11.0"
|
||||
"xmltodict==0.12.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
|
||||
@@ -255,7 +255,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
BluesoundPlayer._TimeoutException):
|
||||
_LOGGER.info("Node %s is offline, retrying later", self._name)
|
||||
await asyncio.sleep(
|
||||
NODE_OFFLINE_CHECK_TIMEOUT, loop=self._hass.loop)
|
||||
NODE_OFFLINE_CHECK_TIMEOUT)
|
||||
self.start_polling()
|
||||
|
||||
except CancelledError:
|
||||
@@ -318,7 +318,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
|
||||
try:
|
||||
websession = async_get_clientsession(self._hass)
|
||||
with async_timeout.timeout(10, loop=self._hass.loop):
|
||||
with async_timeout.timeout(10):
|
||||
response = await websession.get(url)
|
||||
|
||||
if response.status == 200:
|
||||
@@ -361,7 +361,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
|
||||
try:
|
||||
|
||||
with async_timeout.timeout(125, loop=self._hass.loop):
|
||||
with async_timeout.timeout(125):
|
||||
response = await self._polling_session.get(
|
||||
url, headers={CONNECTION: KEEP_ALIVE})
|
||||
|
||||
@@ -378,7 +378,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
self._group_name = group_name
|
||||
# the sleep is needed to make sure that the
|
||||
# devices is synced
|
||||
await asyncio.sleep(1, loop=self._hass.loop)
|
||||
await asyncio.sleep(1)
|
||||
await self.async_trigger_sync_on_all()
|
||||
elif self.is_grouped:
|
||||
# when player is grouped we need to fetch volume from
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.components.device_tracker import (
|
||||
YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
|
||||
load_config, SOURCE_TYPE_BLUETOOTH_LE
|
||||
from homeassistant.components.device_tracker.legacy import (
|
||||
YAML_DEVICES, async_load_config
|
||||
)
|
||||
from homeassistant.components.device_tracker.const import (
|
||||
CONF_TRACK_NEW, CONF_SCAN_INTERVAL, SCAN_INTERVAL, SOURCE_TYPE_BLUETOOTH_LE
|
||||
)
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -79,7 +82,10 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
# Load all known devices.
|
||||
# We just need the devices so set consider_home and home range
|
||||
# to 0
|
||||
for device in load_config(yaml_path, hass, 0):
|
||||
for device in run_coroutine_threadsafe(
|
||||
async_load_config(yaml_path, hass, 0),
|
||||
hass.loop
|
||||
).result():
|
||||
# check if device is a valid bluetooth device
|
||||
if device.mac and device.mac[:4].upper() == BLE_PREFIX:
|
||||
if device.track:
|
||||
@@ -97,7 +103,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
_LOGGER.warning("No Bluetooth LE devices to track!")
|
||||
return False
|
||||
|
||||
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
|
||||
|
||||
def update_ble(now):
|
||||
"""Lookup Bluetooth LE devices and update status."""
|
||||
|
||||
@@ -5,11 +5,16 @@ import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.components.device_tracker import (
|
||||
YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
|
||||
load_config, PLATFORM_SCHEMA, DEFAULT_TRACK_NEW, SOURCE_TYPE_BLUETOOTH,
|
||||
DOMAIN)
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker.legacy import (
|
||||
YAML_DEVICES, async_load_config
|
||||
)
|
||||
from homeassistant.components.device_tracker.const import (
|
||||
CONF_TRACK_NEW, CONF_SCAN_INTERVAL, SCAN_INTERVAL, DEFAULT_TRACK_NEW,
|
||||
SOURCE_TYPE_BLUETOOTH, DOMAIN
|
||||
)
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -60,7 +65,10 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
# Load all known devices.
|
||||
# We just need the devices so set consider_home and home range
|
||||
# to 0
|
||||
for device in load_config(yaml_path, hass, 0):
|
||||
for device in run_coroutine_threadsafe(
|
||||
async_load_config(yaml_path, hass, 0),
|
||||
hass.loop
|
||||
).result():
|
||||
# Check if device is a valid bluetooth device
|
||||
if device.mac and device.mac[:3].upper() == BT_PREFIX:
|
||||
if device.track:
|
||||
@@ -77,7 +85,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
devs_to_track.append(dev[0])
|
||||
see_device(dev[0], dev[1])
|
||||
|
||||
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
|
||||
|
||||
request_rssi = config.get(CONF_REQUEST_RSSI, False)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Bom",
|
||||
"documentation": "https://www.home-assistant.io/components/bom",
|
||||
"requirements": [
|
||||
"bomradarloop==0.1.2"
|
||||
"bomradarloop==0.1.3"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Broadlink",
|
||||
"documentation": "https://www.home-assistant.io/components/broadlink",
|
||||
"requirements": [
|
||||
"broadlink==0.9.0"
|
||||
"broadlink==0.10.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for the Broadlink RM2 Pro (only temperature) and A1 devices."""
|
||||
import binascii
|
||||
import logging
|
||||
import socket
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -60,6 +59,7 @@ class BroadlinkSensor(Entity):
|
||||
"""Initialize the sensor."""
|
||||
self._name = '{} {}'.format(name, SENSOR_TYPES[sensor_type][0])
|
||||
self._state = None
|
||||
self._is_available = False
|
||||
self._type = sensor_type
|
||||
self._broadlink_data = broadlink_data
|
||||
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
|
||||
@@ -74,6 +74,11 @@ class BroadlinkSensor(Entity):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._is_available
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit this state is expressed in."""
|
||||
@@ -83,8 +88,11 @@ class BroadlinkSensor(Entity):
|
||||
"""Get the latest data from the sensor."""
|
||||
self._broadlink_data.update()
|
||||
if self._broadlink_data.data is None:
|
||||
self._state = None
|
||||
self._is_available = False
|
||||
return
|
||||
self._state = self._broadlink_data.data[self._type]
|
||||
self._is_available = True
|
||||
|
||||
|
||||
class BroadlinkData:
|
||||
@@ -119,8 +127,9 @@ class BroadlinkData:
|
||||
if data is not None:
|
||||
self.data = self._schema(data)
|
||||
return
|
||||
except socket.timeout as error:
|
||||
except OSError as error:
|
||||
if retry < 1:
|
||||
self.data = None
|
||||
_LOGGER.error(error)
|
||||
return
|
||||
except (vol.Invalid, vol.MultipleInvalid):
|
||||
@@ -131,7 +140,7 @@ class BroadlinkData:
|
||||
def _auth(self, retry=3):
|
||||
try:
|
||||
auth = self._device.auth()
|
||||
except socket.timeout:
|
||||
except OSError:
|
||||
auth = False
|
||||
if not auth and retry > 0:
|
||||
self._connect()
|
||||
|
||||
@@ -10,9 +10,10 @@ from homeassistant.components.switch import (
|
||||
ENTITY_ID_FORMAT, PLATFORM_SCHEMA, SwitchDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_FRIENDLY_NAME, CONF_HOST, CONF_MAC,
|
||||
CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE)
|
||||
CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE, STATE_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle, slugify
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from . import async_setup_service, data_packet
|
||||
|
||||
@@ -109,13 +110,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
broadlink_device.timeout = config.get(CONF_TIMEOUT)
|
||||
try:
|
||||
broadlink_device.auth()
|
||||
except socket.timeout:
|
||||
except OSError:
|
||||
_LOGGER.error("Failed to connect to device")
|
||||
|
||||
add_entities(switches)
|
||||
|
||||
|
||||
class BroadlinkRMSwitch(SwitchDevice):
|
||||
class BroadlinkRMSwitch(SwitchDevice, RestoreEntity):
|
||||
"""Representation of an Broadlink switch."""
|
||||
|
||||
def __init__(self, name, friendly_name, device, command_on, command_off):
|
||||
@@ -126,6 +127,14 @@ class BroadlinkRMSwitch(SwitchDevice):
|
||||
self._command_on = command_on
|
||||
self._command_off = command_off
|
||||
self._device = device
|
||||
self._is_available = False
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
state = await self.async_get_last_state()
|
||||
if state:
|
||||
self._state = state.state == STATE_ON
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -137,6 +146,11 @@ class BroadlinkRMSwitch(SwitchDevice):
|
||||
"""Return true if unable to access real state of entity."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return not self.should_poll or self._is_available
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
@@ -166,7 +180,7 @@ class BroadlinkRMSwitch(SwitchDevice):
|
||||
return True
|
||||
try:
|
||||
self._device.send_data(packet)
|
||||
except (socket.timeout, ValueError) as error:
|
||||
except (ValueError, OSError) as error:
|
||||
if retry < 1:
|
||||
_LOGGER.error("Error during sending a packet: %s", error)
|
||||
return False
|
||||
@@ -178,7 +192,7 @@ class BroadlinkRMSwitch(SwitchDevice):
|
||||
def _auth(self, retry=2):
|
||||
try:
|
||||
auth = self._device.auth()
|
||||
except socket.timeout:
|
||||
except OSError:
|
||||
auth = False
|
||||
if retry < 1:
|
||||
_LOGGER.error("Timeout during authorization")
|
||||
@@ -244,6 +258,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch):
|
||||
except (socket.timeout, ValueError) as error:
|
||||
if retry < 1:
|
||||
_LOGGER.error("Error during updating the state: %s", error)
|
||||
self._is_available = False
|
||||
return
|
||||
if not self._auth():
|
||||
return
|
||||
@@ -252,6 +267,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch):
|
||||
return self._update(retry-1)
|
||||
self._state = state
|
||||
self._load_power = load_power
|
||||
self._is_available = True
|
||||
|
||||
|
||||
class BroadlinkMP1Slot(BroadlinkRMSwitch):
|
||||
@@ -277,10 +293,12 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch):
|
||||
except (socket.timeout, ValueError) as error:
|
||||
if retry < 1:
|
||||
_LOGGER.error("Error during sending a packet: %s", error)
|
||||
self._is_available = False
|
||||
return False
|
||||
if not self._auth():
|
||||
return False
|
||||
return self._sendpacket(packet, max(0, retry-1))
|
||||
self._is_available = True
|
||||
return True
|
||||
|
||||
@property
|
||||
@@ -330,7 +348,7 @@ class BroadlinkMP1Switch:
|
||||
"""Authenticate the device."""
|
||||
try:
|
||||
auth = self._device.auth()
|
||||
except socket.timeout:
|
||||
except OSError:
|
||||
auth = False
|
||||
if not auth and retry > 0:
|
||||
return self._auth(retry-1)
|
||||
|
||||
@@ -388,7 +388,7 @@ class BrData:
|
||||
tasks.append(dev.async_update_ha_state())
|
||||
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=self.hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
async def schedule_update(self, minute=1):
|
||||
"""Schedule an update after minute minutes."""
|
||||
@@ -407,7 +407,7 @@ class BrData:
|
||||
resp = None
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
with async_timeout.timeout(10):
|
||||
resp = await websession.get(url)
|
||||
|
||||
result[STATUS_CODE] = resp.status
|
||||
|
||||
@@ -8,7 +8,7 @@ import voluptuous as vol
|
||||
from homeassistant.components.calendar import (
|
||||
PLATFORM_SCHEMA, CalendarEventDevice, get_date)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME)
|
||||
CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle, dt
|
||||
|
||||
@@ -36,7 +36,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_SEARCH): cv.string,
|
||||
})
|
||||
]))
|
||||
])),
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean
|
||||
})
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
@@ -50,7 +51,8 @@ def setup_platform(hass, config, add_entities, disc_info=None):
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
client = caldav.DAVClient(url, None, username, password)
|
||||
client = caldav.DAVClient(url, None, username, password,
|
||||
ssl_verify_cert=config.get(CONF_VERIFY_SSL))
|
||||
|
||||
calendars = client.principal().calendars()
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ async def async_setup(hass, config):
|
||||
hass.http.register_view(CalendarEventView(component))
|
||||
|
||||
# Doesn't work in prod builds of the frontend: home-assistant-polymer#1289
|
||||
# await hass.components.frontend.async_register_built_in_panel(
|
||||
# hass.components.frontend.async_register_built_in_panel(
|
||||
# 'calendar', 'calendar', 'hass:calendar')
|
||||
|
||||
await component.async_setup(config)
|
||||
|
||||
@@ -107,11 +107,14 @@ async def async_request_stream(hass, entity_id, fmt):
|
||||
camera = _get_camera_from_entity_id(hass, entity_id)
|
||||
camera_prefs = hass.data[DATA_CAMERA_PREFS].get(entity_id)
|
||||
|
||||
if not camera.stream_source:
|
||||
async with async_timeout.timeout(10):
|
||||
source = await camera.stream_source()
|
||||
|
||||
if not source:
|
||||
raise HomeAssistantError("{} does not support play stream service"
|
||||
.format(camera.entity_id))
|
||||
|
||||
return request_stream(hass, camera.stream_source, fmt=fmt,
|
||||
return request_stream(hass, source, fmt=fmt,
|
||||
keepalive=camera_prefs.preload_stream)
|
||||
|
||||
|
||||
@@ -121,7 +124,7 @@ async def async_get_image(hass, entity_id, timeout=10):
|
||||
camera = _get_camera_from_entity_id(hass, entity_id)
|
||||
|
||||
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
async with async_timeout.timeout(timeout):
|
||||
image = await camera.async_camera_image()
|
||||
|
||||
if image:
|
||||
@@ -221,8 +224,16 @@ async def async_setup(hass, config):
|
||||
async def preload_stream(hass, _):
|
||||
for camera in component.entities:
|
||||
camera_prefs = prefs.get(camera.entity_id)
|
||||
if camera.stream_source and camera_prefs.preload_stream:
|
||||
request_stream(hass, camera.stream_source, keepalive=True)
|
||||
if not camera_prefs.preload_stream:
|
||||
continue
|
||||
|
||||
async with async_timeout.timeout(10):
|
||||
source = await camera.stream_source()
|
||||
|
||||
if not source:
|
||||
continue
|
||||
|
||||
request_stream(hass, source, keepalive=True)
|
||||
|
||||
async_when_setup(hass, DOMAIN_STREAM, preload_stream)
|
||||
|
||||
@@ -328,8 +339,7 @@ class Camera(Entity):
|
||||
"""Return the interval between frames of the mjpeg stream."""
|
||||
return 0.5
|
||||
|
||||
@property
|
||||
def stream_source(self):
|
||||
async def stream_source(self):
|
||||
"""Return the source of the stream."""
|
||||
return None
|
||||
|
||||
@@ -481,7 +491,7 @@ class CameraImageView(CameraView):
|
||||
async def handle(self, request, camera):
|
||||
"""Serve camera image."""
|
||||
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
|
||||
with async_timeout.timeout(10, loop=request.app['hass'].loop):
|
||||
async with async_timeout.timeout(10):
|
||||
image = await camera.async_camera_image()
|
||||
|
||||
if image:
|
||||
@@ -522,12 +532,10 @@ async def websocket_camera_thumbnail(hass, connection, msg):
|
||||
"""
|
||||
try:
|
||||
image = await async_get_image(hass, msg['entity_id'])
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg['id'], {
|
||||
'content_type': image.content_type,
|
||||
'content': base64.b64encode(image.content).decode('utf-8')
|
||||
}
|
||||
))
|
||||
await connection.send_big_result(msg['id'], {
|
||||
'content_type': image.content_type,
|
||||
'content': base64.b64encode(image.content).decode('utf-8')
|
||||
})
|
||||
except HomeAssistantError:
|
||||
connection.send_message(websocket_api.error_message(
|
||||
msg['id'], 'image_fetch_failed', 'Unable to fetch image'))
|
||||
@@ -549,18 +557,25 @@ async def ws_camera_stream(hass, connection, msg):
|
||||
camera = _get_camera_from_entity_id(hass, entity_id)
|
||||
camera_prefs = hass.data[DATA_CAMERA_PREFS].get(entity_id)
|
||||
|
||||
if not camera.stream_source:
|
||||
async with async_timeout.timeout(10):
|
||||
source = await camera.stream_source()
|
||||
|
||||
if not source:
|
||||
raise HomeAssistantError("{} does not support play stream service"
|
||||
.format(camera.entity_id))
|
||||
|
||||
fmt = msg['format']
|
||||
url = request_stream(hass, camera.stream_source, fmt=fmt,
|
||||
url = request_stream(hass, source, fmt=fmt,
|
||||
keepalive=camera_prefs.preload_stream)
|
||||
connection.send_result(msg['id'], {'url': url})
|
||||
except HomeAssistantError as ex:
|
||||
_LOGGER.error(ex)
|
||||
_LOGGER.error("Error requesting stream: %s", ex)
|
||||
connection.send_error(
|
||||
msg['id'], 'start_stream_failed', str(ex))
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error("Timeout getting stream source")
|
||||
connection.send_error(
|
||||
msg['id'], 'start_stream_failed', "Timeout getting stream source")
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
@@ -624,7 +639,10 @@ async def async_handle_snapshot_service(camera, service):
|
||||
|
||||
async def async_handle_play_stream_service(camera, service_call):
|
||||
"""Handle play stream services calls."""
|
||||
if not camera.stream_source:
|
||||
async with async_timeout.timeout(10):
|
||||
source = await camera.stream_source()
|
||||
|
||||
if not source:
|
||||
raise HomeAssistantError("{} does not support play stream service"
|
||||
.format(camera.entity_id))
|
||||
|
||||
@@ -633,7 +651,7 @@ async def async_handle_play_stream_service(camera, service_call):
|
||||
fmt = service_call.data[ATTR_FORMAT]
|
||||
entity_ids = service_call.data[ATTR_MEDIA_PLAYER]
|
||||
|
||||
url = request_stream(hass, camera.stream_source, fmt=fmt,
|
||||
url = request_stream(hass, source, fmt=fmt,
|
||||
keepalive=camera_prefs.preload_stream)
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity_ids,
|
||||
@@ -648,7 +666,10 @@ async def async_handle_play_stream_service(camera, service_call):
|
||||
|
||||
async def async_handle_record_service(camera, call):
|
||||
"""Handle stream recording service calls."""
|
||||
if not camera.stream_source:
|
||||
async with async_timeout.timeout(10):
|
||||
source = await camera.stream_source()
|
||||
|
||||
if not source:
|
||||
raise HomeAssistantError("{} does not support record service"
|
||||
.format(camera.entity_id))
|
||||
|
||||
@@ -659,7 +680,7 @@ async def async_handle_record_service(camera, call):
|
||||
variables={ATTR_ENTITY_ID: camera})
|
||||
|
||||
data = {
|
||||
CONF_STREAM_SOURCE: camera.stream_source,
|
||||
CONF_STREAM_SOURCE: source,
|
||||
CONF_FILENAME: video_path,
|
||||
CONF_DURATION: call.data[CONF_DURATION],
|
||||
CONF_LOOKBACK: call.data[CONF_LOOKBACK],
|
||||
|
||||
@@ -79,7 +79,7 @@ class CanaryCamera(Camera):
|
||||
image = await asyncio.shield(ffmpeg.get_image(
|
||||
self._live_stream_session.live_stream_url,
|
||||
output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
|
||||
extra_cmd=self._ffmpeg_arguments))
|
||||
return image
|
||||
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Component to embed Google Cast."""
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
|
||||
DOMAIN = 'cast'
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
@@ -23,15 +22,3 @@ async def async_setup_entry(hass, entry):
|
||||
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
||||
entry, 'media_player'))
|
||||
return True
|
||||
|
||||
|
||||
async def _async_has_devices(hass):
|
||||
"""Return if there are devices that can be discovered."""
|
||||
from pychromecast.discovery import discover_chromecasts
|
||||
|
||||
return await hass.async_add_executor_job(discover_chromecasts)
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN, 'Google Cast', _async_has_devices,
|
||||
config_entries.CONN_CLASS_LOCAL_PUSH)
|
||||
|
||||
16
homeassistant/components/cast/config_flow.py
Normal file
16
homeassistant/components/cast/config_flow.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Config flow for Cast."""
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant import config_entries
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def _async_has_devices(hass):
|
||||
"""Return if there are devices that can be discovered."""
|
||||
from pychromecast.discovery import discover_chromecasts
|
||||
|
||||
return await hass.async_add_executor_job(discover_chromecasts)
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN, 'Google Cast', _async_has_devices,
|
||||
config_entries.CONN_CLASS_LOCAL_PUSH)
|
||||
3
homeassistant/components/cast/const.py
Normal file
3
homeassistant/components/cast/const.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Consts for Cast integration."""
|
||||
|
||||
DOMAIN = 'cast'
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"domain": "cast",
|
||||
"name": "Cast",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/cast",
|
||||
"requirements": [
|
||||
"pychromecast==3.2.0"
|
||||
"pychromecast==3.2.1"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
|
||||
@@ -24,6 +24,7 @@ from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect, dispatcher_send)
|
||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.logging import async_create_catching_coro
|
||||
|
||||
from . import DOMAIN as CAST_DOMAIN
|
||||
|
||||
@@ -522,8 +523,8 @@ class CastDevice(MediaPlayerDevice):
|
||||
if _is_matching_dynamic_group(self._cast_info, discover):
|
||||
_LOGGER.debug("Discovered matching dynamic group: %s",
|
||||
discover)
|
||||
self.hass.async_create_task(
|
||||
self.async_set_dynamic_group(discover))
|
||||
self.hass.async_create_task(async_create_catching_coro(
|
||||
self.async_set_dynamic_group(discover)))
|
||||
return
|
||||
|
||||
if self._cast_info.uuid != discover.uuid:
|
||||
@@ -536,7 +537,8 @@ class CastDevice(MediaPlayerDevice):
|
||||
self._cast_info.host, self._cast_info.port)
|
||||
return
|
||||
_LOGGER.debug("Discovered chromecast with same UUID: %s", discover)
|
||||
self.hass.async_create_task(self.async_set_cast_info(discover))
|
||||
self.hass.async_create_task(async_create_catching_coro(
|
||||
self.async_set_cast_info(discover)))
|
||||
|
||||
def async_cast_removed(discover: ChromecastInfo):
|
||||
"""Handle removal of Chromecast."""
|
||||
@@ -546,13 +548,15 @@ class CastDevice(MediaPlayerDevice):
|
||||
if (self._dynamic_group_cast_info is not None and
|
||||
self._dynamic_group_cast_info.uuid == discover.uuid):
|
||||
_LOGGER.debug("Removed matching dynamic group: %s", discover)
|
||||
self.hass.async_create_task(self.async_del_dynamic_group())
|
||||
self.hass.async_create_task(async_create_catching_coro(
|
||||
self.async_del_dynamic_group()))
|
||||
return
|
||||
if self._cast_info.uuid != discover.uuid:
|
||||
# Removed is not our device.
|
||||
return
|
||||
_LOGGER.debug("Removed chromecast with same UUID: %s", discover)
|
||||
self.hass.async_create_task(self.async_del_cast_info(discover))
|
||||
self.hass.async_create_task(async_create_catching_coro(
|
||||
self.async_del_cast_info(discover)))
|
||||
|
||||
async def async_stop(event):
|
||||
"""Disconnect socket on Home Assistant stop."""
|
||||
@@ -565,14 +569,15 @@ class CastDevice(MediaPlayerDevice):
|
||||
self.hass, SIGNAL_CAST_REMOVED,
|
||||
async_cast_removed)
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop)
|
||||
self.hass.async_create_task(self.async_set_cast_info(self._cast_info))
|
||||
self.hass.async_create_task(async_create_catching_coro(
|
||||
self.async_set_cast_info(self._cast_info)))
|
||||
for info in self.hass.data[KNOWN_CHROMECAST_INFO_KEY]:
|
||||
if _is_matching_dynamic_group(self._cast_info, info):
|
||||
_LOGGER.debug("[%s %s (%s:%s)] Found dynamic group: %s",
|
||||
self.entity_id, self._cast_info.friendly_name,
|
||||
self._cast_info.host, self._cast_info.port, info)
|
||||
self.hass.async_create_task(
|
||||
self.async_set_dynamic_group(info))
|
||||
self.hass.async_create_task(async_create_catching_coro(
|
||||
self.async_set_dynamic_group(info)))
|
||||
break
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
@@ -659,7 +664,7 @@ class CastDevice(MediaPlayerDevice):
|
||||
self.entity_id, self._cast_info.friendly_name,
|
||||
self._cast_info.host, self._cast_info.port, cast_info)
|
||||
|
||||
self.async_del_dynamic_group()
|
||||
await self.async_del_dynamic_group()
|
||||
self._dynamic_group_cast_info = cast_info
|
||||
|
||||
# pylint: disable=protected-access
|
||||
@@ -1046,6 +1051,11 @@ class CastDevice(MediaPlayerDevice):
|
||||
|
||||
return images[0].url if images and images[0].url else None
|
||||
|
||||
@property
|
||||
def media_image_remotely_accessible(self) -> bool:
|
||||
"""If the image url is remotely accessible."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user