mirror of
https://github.com/home-assistant/core.git
synced 2026-01-04 23:05:26 +01:00
Compare commits
480 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd7fff2ce8 | ||
|
|
4e58eb8bae | ||
|
|
49121f2347 | ||
|
|
708ababd78 | ||
|
|
92c0f9e4aa | ||
|
|
81cac33801 | ||
|
|
1e3930a447 | ||
|
|
3cde8dc3a9 | ||
|
|
36c31a6293 | ||
|
|
8aa2cefd75 | ||
|
|
3b53003795 | ||
|
|
377730a37c | ||
|
|
d9c7f777c5 | ||
|
|
b7742999cf | ||
|
|
8742750926 | ||
|
|
e87ecbd500 | ||
|
|
3838be4cb8 | ||
|
|
0ddd502d00 | ||
|
|
d88040eeed | ||
|
|
44b33d45b1 | ||
|
|
9b53b7e9e4 | ||
|
|
80cd8b180c | ||
|
|
b3e37af9b1 | ||
|
|
57f7e7eedc | ||
|
|
14ad7428ea | ||
|
|
f86083cf52 | ||
|
|
3891f2eebe | ||
|
|
de9bac9ee3 | ||
|
|
01953ab46b | ||
|
|
fc4dd4e51f | ||
|
|
90f3f2b1e7 | ||
|
|
c1ca7beea1 | ||
|
|
84fd66c8a1 | ||
|
|
97c493448b | ||
|
|
9fa34f0d77 | ||
|
|
cdcc818bf9 | ||
|
|
83b4e56978 | ||
|
|
089a2f4e71 | ||
|
|
f241becf7f | ||
|
|
7e702d3caa | ||
|
|
ab8c127a4a | ||
|
|
afe21b4408 | ||
|
|
b066877453 | ||
|
|
796933de68 | ||
|
|
8f59be2059 | ||
|
|
dfb8f60fe2 | ||
|
|
3f747f1a8c | ||
|
|
4751ad69a7 | ||
|
|
c6ca27e9b4 | ||
|
|
e73b9b9b8f | ||
|
|
6b2f50b29e | ||
|
|
fcd756d58a | ||
|
|
24db2b66ab | ||
|
|
320efdb744 | ||
|
|
9e0497875e | ||
|
|
30806fa362 | ||
|
|
9f51deb1de | ||
|
|
0ca94f239d | ||
|
|
ed7aea006a | ||
|
|
b7b8296c73 | ||
|
|
afb3a52b5b | ||
|
|
4446b15cb0 | ||
|
|
d1b5bc19da | ||
|
|
75bb78d440 | ||
|
|
2d870a29c4 | ||
|
|
7cb7c76a83 | ||
|
|
b40b934029 | ||
|
|
5ffcb99b4f | ||
|
|
69d358fa08 | ||
|
|
f36b94b376 | ||
|
|
b5d4e18880 | ||
|
|
43271ca0f7 | ||
|
|
7659c33439 | ||
|
|
b8ddbc3fdb | ||
|
|
2aa2233d9b | ||
|
|
6bcba1fbea | ||
|
|
466d3a5ef8 | ||
|
|
8aa1283adc | ||
|
|
312872961f | ||
|
|
00235cf6f0 | ||
|
|
80e616cacf | ||
|
|
c7ac216602 | ||
|
|
0d43cb6d0e | ||
|
|
d2e102ee2f | ||
|
|
d2907b8e53 | ||
|
|
419400f90b | ||
|
|
532a75b487 | ||
|
|
291fba0ba4 | ||
|
|
597da90622 | ||
|
|
f14251bdcc | ||
|
|
ebdfb56803 | ||
|
|
7aa41d66e9 | ||
|
|
7113ec6073 | ||
|
|
f78dcb96b0 | ||
|
|
996da72a4c | ||
|
|
8547489014 | ||
|
|
c6683cba7d | ||
|
|
ea4480f170 | ||
|
|
0ab81b03a8 | ||
|
|
d0463942be | ||
|
|
275b485b36 | ||
|
|
15c77fe548 | ||
|
|
e5930da972 | ||
|
|
8fb6030f97 | ||
|
|
9eac11dcbe | ||
|
|
afd9c44ffb | ||
|
|
1f06d6ac1a | ||
|
|
ca86755409 | ||
|
|
1f476936a2 | ||
|
|
ddeeba20b9 | ||
|
|
5129a48750 | ||
|
|
95eae47438 | ||
|
|
372470f52a | ||
|
|
02cc6a2f9a | ||
|
|
9cb6464c58 | ||
|
|
5b9a9d8e04 | ||
|
|
9411fca955 | ||
|
|
b8c06ad019 | ||
|
|
9c92151ad1 | ||
|
|
f0a0ce504b | ||
|
|
d9533127f9 | ||
|
|
fa127188df | ||
|
|
667b41dd4a | ||
|
|
f236e14bd6 | ||
|
|
e75f9b36f9 | ||
|
|
132bb7902a | ||
|
|
df2ab62ce9 | ||
|
|
f7c99ada9d | ||
|
|
8bd281d5a3 | ||
|
|
210eab16da | ||
|
|
64ada1ea5a | ||
|
|
14ad5c0006 | ||
|
|
d34c47a9e1 | ||
|
|
f971309113 | ||
|
|
f8ca4cfd91 | ||
|
|
4324d87673 | ||
|
|
7f48a280ee | ||
|
|
f4c35a389d | ||
|
|
8ab2f669d2 | ||
|
|
de37fc90c0 | ||
|
|
c571637176 | ||
|
|
b803075eb4 | ||
|
|
ae85baf396 | ||
|
|
05eac915d1 | ||
|
|
8f107c46fe | ||
|
|
fd2987e551 | ||
|
|
9472529d43 | ||
|
|
f7f0a4e811 | ||
|
|
54b0cde52a | ||
|
|
a016dd2140 | ||
|
|
878e369c4a | ||
|
|
599542394a | ||
|
|
7fed49c4ab | ||
|
|
954191c385 | ||
|
|
e2fca0691e | ||
|
|
f24979c7cf | ||
|
|
5bab0018f5 | ||
|
|
f541b101c9 | ||
|
|
0bf054fb59 | ||
|
|
aa4da479b5 | ||
|
|
d93716bd84 | ||
|
|
f99701f41a | ||
|
|
29be78e08e | ||
|
|
ec732c896d | ||
|
|
00c1b40940 | ||
|
|
4287d1dd2d | ||
|
|
e9b8b290fc | ||
|
|
06b9600069 | ||
|
|
cff4755708 | ||
|
|
17f04c1736 | ||
|
|
0b6aa38b13 | ||
|
|
782f5c7d19 | ||
|
|
5cee9942a6 | ||
|
|
65be458ce0 | ||
|
|
ce069be16e | ||
|
|
0d7cb54872 | ||
|
|
6935b62487 | ||
|
|
e698fc2553 | ||
|
|
df3d82e0e3 | ||
|
|
35ae85e14e | ||
|
|
c89dade619 | ||
|
|
bdba3852d0 | ||
|
|
c41ca37a04 | ||
|
|
917ebed4c9 | ||
|
|
43ae57cc59 | ||
|
|
f4d3d5904e | ||
|
|
bde02afe4f | ||
|
|
42fea4fb97 | ||
|
|
52074ee9bb | ||
|
|
eb385515c8 | ||
|
|
589764900a | ||
|
|
9329ec2486 | ||
|
|
47af194d06 | ||
|
|
39412dc930 | ||
|
|
2e517ab6bc | ||
|
|
7933bd7f91 | ||
|
|
58c77e1f55 | ||
|
|
e3a8f3a106 | ||
|
|
1aba4699b9 | ||
|
|
114bc8ec18 | ||
|
|
c6f3c239bb | ||
|
|
34d7758b4a | ||
|
|
2c36b9db1f | ||
|
|
24efda20bf | ||
|
|
3322fee814 | ||
|
|
4581a741bd | ||
|
|
b506aafbb4 | ||
|
|
121ec5c684 | ||
|
|
087bffeaae | ||
|
|
2bf2214d51 | ||
|
|
ddee5f8b86 | ||
|
|
c5d0440041 | ||
|
|
24c110ad3c | ||
|
|
7077e19cf8 | ||
|
|
6f568d1cf6 | ||
|
|
d951ed4d68 | ||
|
|
3366d2c1ad | ||
|
|
46b5b6240f | ||
|
|
30fccc696e | ||
|
|
fb947288ad | ||
|
|
be3800d9a5 | ||
|
|
de79c42b8a | ||
|
|
b3bd59efb0 | ||
|
|
31737c5100 | ||
|
|
dbf6b01a60 | ||
|
|
abf147ed57 | ||
|
|
c59b038512 | ||
|
|
93b16e7efb | ||
|
|
561f6996c6 | ||
|
|
b261c4b7f8 | ||
|
|
26ba4a56e8 | ||
|
|
dcdae325ea | ||
|
|
3d4ff74761 | ||
|
|
81fa74e5ca | ||
|
|
f9f53fd278 | ||
|
|
36524e9d3f | ||
|
|
bf54582d76 | ||
|
|
8de79ed57c | ||
|
|
8ee0e0c6c6 | ||
|
|
a901c594a9 | ||
|
|
2e9132873a | ||
|
|
6e4ce35a69 | ||
|
|
1c3ef8be55 | ||
|
|
922f34f72d | ||
|
|
959fa81ea6 | ||
|
|
9a6c229b1d | ||
|
|
4a7507bcea | ||
|
|
44556a86e3 | ||
|
|
e161dc3b77 | ||
|
|
dbf721cd2c | ||
|
|
0992e83f8d | ||
|
|
a498e15910 | ||
|
|
27e159f63f | ||
|
|
7b53238f9b | ||
|
|
075169a7a9 | ||
|
|
3abe49bace | ||
|
|
eb0d989c88 | ||
|
|
42cb23f768 | ||
|
|
5418e0510d | ||
|
|
164c68093b | ||
|
|
5dd691e55d | ||
|
|
155df912e5 | ||
|
|
610b0b6494 | ||
|
|
f76ccb636c | ||
|
|
b2bdf05cae | ||
|
|
461e6acf5c | ||
|
|
fbcf0880f3 | ||
|
|
9bf824bf00 | ||
|
|
0009e7bde9 | ||
|
|
67e62e8020 | ||
|
|
e8e135fd25 | ||
|
|
63e53fdf15 | ||
|
|
ed3ca2b74f | ||
|
|
91a93b0060 | ||
|
|
6d3167fcd4 | ||
|
|
255607f3a5 | ||
|
|
9807ba1a5d | ||
|
|
782a90a535 | ||
|
|
7caddd48cd | ||
|
|
5c99862878 | ||
|
|
6a5f9faa33 | ||
|
|
03d94df3cd | ||
|
|
45484ba569 | ||
|
|
92c536ec0e | ||
|
|
1f290bad94 | ||
|
|
dd938d7460 | ||
|
|
73ed2ab164 | ||
|
|
283407fe6c | ||
|
|
97e928df4a | ||
|
|
a39846bad9 | ||
|
|
3fe895c18f | ||
|
|
0a301f7dcb | ||
|
|
58c7ee649d | ||
|
|
93689d68f7 | ||
|
|
8613694544 | ||
|
|
cb7ae5cdf2 | ||
|
|
a4c0c34028 | ||
|
|
6eba7c4ff3 | ||
|
|
82edea6077 | ||
|
|
9b47af68ae | ||
|
|
02b46e2ba3 | ||
|
|
e9ae862fca | ||
|
|
31dc6832e7 | ||
|
|
c3e3f662f4 | ||
|
|
bcea3a9cba | ||
|
|
23290fa6ee | ||
|
|
4ee21e66dc | ||
|
|
4a3f754033 | ||
|
|
c75c00d568 | ||
|
|
a9361482d9 | ||
|
|
83e83520e6 | ||
|
|
65e6c50748 | ||
|
|
2a76347071 | ||
|
|
c5f9220500 | ||
|
|
afc109a585 | ||
|
|
a69c3953f1 | ||
|
|
a3e77bc5f3 | ||
|
|
957320f265 | ||
|
|
2fce79eccf | ||
|
|
caa48fab13 | ||
|
|
19ebdf2cf1 | ||
|
|
e9f96bfd7f | ||
|
|
ab8299b6cf | ||
|
|
1c5800d98b | ||
|
|
bfa86b8138 | ||
|
|
4163889c6b | ||
|
|
329d128e03 | ||
|
|
f516550f9f | ||
|
|
32ee4f0714 | ||
|
|
3d1a324f33 | ||
|
|
a9140dc8f5 | ||
|
|
145677ed75 | ||
|
|
7363378ac4 | ||
|
|
93706fa568 | ||
|
|
b763c0f902 | ||
|
|
1578187376 | ||
|
|
b12e79e5cf | ||
|
|
239e314dc1 | ||
|
|
e85e5789a2 | ||
|
|
9c840f93f0 | ||
|
|
dcc46226ee | ||
|
|
314e5ac296 | ||
|
|
9c77465c0e | ||
|
|
4073f63256 | ||
|
|
9565c0bd1d | ||
|
|
9cb5ea20af | ||
|
|
eef9246db1 | ||
|
|
865ea82432 | ||
|
|
4d9ef9e795 | ||
|
|
87bd2a32e4 | ||
|
|
f0693f6f91 | ||
|
|
3de822a0e2 | ||
|
|
71b56363d3 | ||
|
|
f400925825 | ||
|
|
20fb7b59ef | ||
|
|
622f23abd7 | ||
|
|
5337d0b4f3 | ||
|
|
b71dc752fa | ||
|
|
e16793013a | ||
|
|
98163504fb | ||
|
|
32cb666dac | ||
|
|
03dd1e6870 | ||
|
|
cb73a8bbb0 | ||
|
|
3169c0416e | ||
|
|
2f71f8908b | ||
|
|
c38a0f1bf0 | ||
|
|
deeb288daf | ||
|
|
d6913c6914 | ||
|
|
af5eacf303 | ||
|
|
a87a5d266e | ||
|
|
027f173a08 | ||
|
|
6ae345b01c | ||
|
|
b03e6050c5 | ||
|
|
98dfbf2565 | ||
|
|
3740424725 | ||
|
|
360addfb0b | ||
|
|
f4ac317d64 | ||
|
|
d1ef875132 | ||
|
|
96c5e4c507 | ||
|
|
851d7e22e7 | ||
|
|
e5c97fdcab | ||
|
|
3e6de21302 | ||
|
|
4579717317 | ||
|
|
3802fec568 | ||
|
|
b62b3b26f2 | ||
|
|
62752e0065 | ||
|
|
df65d2151d | ||
|
|
c9c707e368 | ||
|
|
0f877711a0 | ||
|
|
60080a529d | ||
|
|
38576e5b74 | ||
|
|
3f3955c1cd | ||
|
|
6cb735271f | ||
|
|
0acd4b28f9 | ||
|
|
cbadd64b28 | ||
|
|
9cd8a86eb4 | ||
|
|
d6e4208665 | ||
|
|
649bc55a3b | ||
|
|
a22aad50e1 | ||
|
|
92e9c2aa72 | ||
|
|
2adf5918f5 | ||
|
|
21870e2167 | ||
|
|
6b7cbca04c | ||
|
|
8a4c78b69f | ||
|
|
cfbd84f450 | ||
|
|
9f146a3954 | ||
|
|
3175627363 | ||
|
|
86d7bc4962 | ||
|
|
434c848104 | ||
|
|
92bad453f2 | ||
|
|
644c33cc1e | ||
|
|
b7896491e3 | ||
|
|
cce8b1183f | ||
|
|
e276e899cf | ||
|
|
3f4798b5c3 | ||
|
|
714d44c503 | ||
|
|
c00da509a1 | ||
|
|
9e286d7c1f | ||
|
|
9e33398a7b | ||
|
|
47003fc04f | ||
|
|
b7b62a90e2 | ||
|
|
901c4f18cb | ||
|
|
43048962f2 | ||
|
|
77bf10e37c | ||
|
|
9c7d3c2a63 | ||
|
|
312d49caec | ||
|
|
b5284aa445 | ||
|
|
b6e8cafdea | ||
|
|
3c68db32d6 | ||
|
|
544a3b929f | ||
|
|
67d92c4f5d | ||
|
|
8bebfba21a | ||
|
|
5024a80d61 | ||
|
|
599390d985 | ||
|
|
577cf0991f | ||
|
|
aa157e17f9 | ||
|
|
bd23145331 | ||
|
|
a629e1bec2 | ||
|
|
ec7d33f277 | ||
|
|
c099c259ea | ||
|
|
52974ff742 | ||
|
|
54d463e746 | ||
|
|
c7c0ed89c8 | ||
|
|
8283f50e22 | ||
|
|
86e67e4712 | ||
|
|
ad3d7c9523 | ||
|
|
a10fb94e9e | ||
|
|
08fe7c3ece | ||
|
|
4222f7562b | ||
|
|
6ac9677168 | ||
|
|
fc8af22191 | ||
|
|
0f69be117f | ||
|
|
2734a30f37 | ||
|
|
65a8882426 | ||
|
|
7be7a8de30 | ||
|
|
f9973696f3 | ||
|
|
9798ff019f | ||
|
|
f6f549dc3c | ||
|
|
4a757b7994 | ||
|
|
37a667c2de | ||
|
|
398ea40189 | ||
|
|
cf0bd6470a | ||
|
|
0723c7f5dc | ||
|
|
7def587c93 | ||
|
|
d5a5695411 | ||
|
|
277a9a3995 | ||
|
|
50f0eac7f3 | ||
|
|
44e35b7f52 | ||
|
|
324587b2db | ||
|
|
ad3d0c4e99 | ||
|
|
3014930371 | ||
|
|
b773a9049c | ||
|
|
4e8cd7281c | ||
|
|
fd9370da39 | ||
|
|
399f8a72c3 | ||
|
|
75e42acfe7 | ||
|
|
42a444712b | ||
|
|
61a96aecc0 | ||
|
|
96105ef6e7 | ||
|
|
0524c51c1a |
52
.coveragerc
52
.coveragerc
@@ -76,16 +76,13 @@ omit =
|
||||
homeassistant/components/daikin.py
|
||||
homeassistant/components/*/daikin.py
|
||||
|
||||
homeassistant/components/deconz/*
|
||||
homeassistant/components/*/deconz.py
|
||||
|
||||
homeassistant/components/digital_ocean.py
|
||||
homeassistant/components/*/digital_ocean.py
|
||||
|
||||
homeassistant/components/dominos.py
|
||||
|
||||
homeassistant/components/doorbird.py
|
||||
homeassistant/components/*/doorbird.py
|
||||
homeassistant/components/doorbird.py
|
||||
homeassistant/components/*/doorbird.py
|
||||
|
||||
homeassistant/components/dweet.py
|
||||
homeassistant/components/*/dweet.py
|
||||
@@ -123,12 +120,18 @@ omit =
|
||||
homeassistant/components/eufy.py
|
||||
homeassistant/components/*/eufy.py
|
||||
|
||||
homeassistant/components/fibaro.py
|
||||
homeassistant/components/*/fibaro.py
|
||||
|
||||
homeassistant/components/gc100.py
|
||||
homeassistant/components/*/gc100.py
|
||||
|
||||
homeassistant/components/google.py
|
||||
homeassistant/components/*/google.py
|
||||
|
||||
homeassistant/components/greeneye_monitor.py
|
||||
homeassistant/components/sensor/greeneye_monitor.py
|
||||
|
||||
homeassistant/components/habitica/*
|
||||
homeassistant/components/*/habitica.py
|
||||
|
||||
@@ -203,13 +206,15 @@ omit =
|
||||
homeassistant/components/logi_circle.py
|
||||
homeassistant/components/*/logi_circle.py
|
||||
|
||||
homeassistant/components/lupusec.py
|
||||
homeassistant/components/*/lupusec.py
|
||||
|
||||
homeassistant/components/lutron.py
|
||||
homeassistant/components/*/lutron.py
|
||||
|
||||
homeassistant/components/lutron_caseta.py
|
||||
homeassistant/components/*/lutron_caseta.py
|
||||
|
||||
homeassistant/components/mailgun.py
|
||||
homeassistant/components/*/mailgun.py
|
||||
|
||||
homeassistant/components/matrix.py
|
||||
@@ -248,7 +253,7 @@ omit =
|
||||
homeassistant/components/opencv.py
|
||||
homeassistant/components/*/opencv.py
|
||||
|
||||
homeassistant/components/opentherm_gw.py
|
||||
homeassistant/components/opentherm_gw/*
|
||||
homeassistant/components/*/opentherm_gw.py
|
||||
|
||||
homeassistant/components/openuv/__init__.py
|
||||
@@ -257,6 +262,10 @@ omit =
|
||||
homeassistant/components/pilight.py
|
||||
homeassistant/components/*/pilight.py
|
||||
|
||||
homeassistant/components/point/__init__.py
|
||||
homeassistant/components/point/const.py
|
||||
homeassistant/components/*/point.py
|
||||
|
||||
homeassistant/components/switch/qwikswitch.py
|
||||
homeassistant/components/light/qwikswitch.py
|
||||
|
||||
@@ -266,7 +275,7 @@ omit =
|
||||
homeassistant/components/raincloud.py
|
||||
homeassistant/components/*/raincloud.py
|
||||
|
||||
homeassistant/components/rainmachine/*
|
||||
homeassistant/components/rainmachine/__init__.py
|
||||
homeassistant/components/*/rainmachine.py
|
||||
|
||||
homeassistant/components/raspihats.py
|
||||
@@ -290,6 +299,9 @@ omit =
|
||||
homeassistant/components/scsgate.py
|
||||
homeassistant/components/*/scsgate.py
|
||||
|
||||
homeassistant/components/sense.py
|
||||
homeassistant/components/*/sense.py
|
||||
|
||||
homeassistant/components/simplisafe/__init__.py
|
||||
homeassistant/components/*/simplisafe.py
|
||||
|
||||
@@ -331,10 +343,12 @@ omit =
|
||||
homeassistant/components/toon.py
|
||||
homeassistant/components/*/toon.py
|
||||
|
||||
homeassistant/components/tplink_lte.py
|
||||
homeassistant/components/*/tplink_lte.py
|
||||
|
||||
homeassistant/components/tradfri.py
|
||||
homeassistant/components/*/tradfri.py
|
||||
|
||||
homeassistant/components/twilio.py
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twilio_call.py
|
||||
|
||||
@@ -364,6 +378,9 @@ omit =
|
||||
|
||||
homeassistant/components/*/webostv.py
|
||||
|
||||
homeassistant/components/w800rf32.py
|
||||
homeassistant/components/*/w800rf32.py
|
||||
|
||||
homeassistant/components/wemo.py
|
||||
homeassistant/components/*/wemo.py
|
||||
|
||||
@@ -467,11 +484,13 @@ omit =
|
||||
homeassistant/components/device_tracker/bluetooth_le_tracker.py
|
||||
homeassistant/components/device_tracker/bluetooth_tracker.py
|
||||
homeassistant/components/device_tracker/bt_home_hub_5.py
|
||||
homeassistant/components/device_tracker/bt_smarthub.py
|
||||
homeassistant/components/device_tracker/cisco_ios.py
|
||||
homeassistant/components/device_tracker/ddwrt.py
|
||||
homeassistant/components/device_tracker/freebox.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/google_maps.py
|
||||
homeassistant/components/device_tracker/googlehome.py
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
homeassistant/components/device_tracker/hitron_coda.py
|
||||
homeassistant/components/device_tracker/huawei_router.py
|
||||
@@ -494,12 +513,14 @@ omit =
|
||||
homeassistant/components/device_tracker/tile.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/traccar.py
|
||||
homeassistant/components/device_tracker/trackr.py
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/emoncms_history.py
|
||||
homeassistant/components/emulated_hue/upnp.py
|
||||
homeassistant/components/fan/mqtt.py
|
||||
homeassistant/components/fan/wemo.py
|
||||
homeassistant/components/folder_watcher.py
|
||||
homeassistant/components/foursquare.py
|
||||
homeassistant/components/goalfeed.py
|
||||
@@ -507,6 +528,7 @@ omit =
|
||||
homeassistant/components/image_processing/dlib_face_detect.py
|
||||
homeassistant/components/image_processing/dlib_face_identify.py
|
||||
homeassistant/components/image_processing/seven_segments.py
|
||||
homeassistant/components/image_processing/tensorflow.py
|
||||
homeassistant/components/keyboard_remote.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/light/avion.py
|
||||
@@ -526,6 +548,7 @@ omit =
|
||||
homeassistant/components/light/lw12wifi.py
|
||||
homeassistant/components/light/mystrom.py
|
||||
homeassistant/components/light/nanoleaf_aurora.py
|
||||
homeassistant/components/light/niko_home_control.py
|
||||
homeassistant/components/light/opple.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/light/piglow.py
|
||||
@@ -568,7 +591,7 @@ omit =
|
||||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/lg_netcast.py
|
||||
homeassistant/components/media_player/lg_soundbar.py
|
||||
homeassistant/components/media_player/lg_soundbar.py
|
||||
homeassistant/components/media_player/liveboxplaytv.py
|
||||
homeassistant/components/media_player/mediaroom.py
|
||||
homeassistant/components/media_player/mpchc.py
|
||||
@@ -577,6 +600,7 @@ omit =
|
||||
homeassistant/components/media_player/nadtcp.py
|
||||
homeassistant/components/media_player/onkyo.py
|
||||
homeassistant/components/media_player/openhome.py
|
||||
homeassistant/components/media_player/panasonic_bluray.py
|
||||
homeassistant/components/media_player/panasonic_viera.py
|
||||
homeassistant/components/media_player/pandora.py
|
||||
homeassistant/components/media_player/philips_js.py
|
||||
@@ -692,6 +716,7 @@ omit =
|
||||
homeassistant/components/sensor/fints.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/flunearyou.py
|
||||
homeassistant/components/sensor/folder.py
|
||||
homeassistant/components/sensor/foobot.py
|
||||
homeassistant/components/sensor/fritzbox_callmonitor.py
|
||||
@@ -716,12 +741,14 @@ omit =
|
||||
homeassistant/components/sensor/kwb.py
|
||||
homeassistant/components/sensor/lacrosse.py
|
||||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/launch_library.py
|
||||
homeassistant/components/sensor/linky.py
|
||||
homeassistant/components/sensor/linux_battery.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/luftdaten.py
|
||||
homeassistant/components/sensor/lyft.py
|
||||
homeassistant/components/sensor/magicseaweed.py
|
||||
homeassistant/components/sensor/meteo_france.py
|
||||
homeassistant/components/sensor/metoffice.py
|
||||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/mitemp_bt.py
|
||||
@@ -758,11 +785,12 @@ omit =
|
||||
homeassistant/components/sensor/rainbird.py
|
||||
homeassistant/components/sensor/ripple.py
|
||||
homeassistant/components/sensor/rtorrent.py
|
||||
homeassistant/components/sensor/ruter.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sense.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/serial.py
|
||||
homeassistant/components/sensor/seventeentrack.py
|
||||
homeassistant/components/sensor/sht31.py
|
||||
homeassistant/components/sensor/shodan.py
|
||||
homeassistant/components/sensor/sigfox.py
|
||||
@@ -782,9 +810,11 @@ omit =
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/syncthru.py
|
||||
homeassistant/components/sensor/synologydsm.py
|
||||
homeassistant/components/sensor/srp_energy.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/sytadin.py
|
||||
homeassistant/components/sensor/tank_utility.py
|
||||
homeassistant/components/sensor/tautulli.py
|
||||
homeassistant/components/sensor/ted5000.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
homeassistant/components/sensor/thermoworks_smoke.py
|
||||
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -13,6 +13,7 @@
|
||||
## Checklist:
|
||||
- [ ] 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.
|
||||
|
||||
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)
|
||||
|
||||
10
.readthedocs.yml
Normal file
10
.readthedocs.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
# .readthedocs.yml
|
||||
|
||||
build:
|
||||
image: latest
|
||||
|
||||
python:
|
||||
version: 3.6
|
||||
setup_py_install: true
|
||||
|
||||
requirements_file: requirements_docs.txt
|
||||
14
CODEOWNERS
14
CODEOWNERS
@@ -56,17 +56,21 @@ homeassistant/components/climate/ephember.py @ttroy50
|
||||
homeassistant/components/climate/eq3btsmart.py @rytilahti
|
||||
homeassistant/components/climate/mill.py @danielhiversen
|
||||
homeassistant/components/climate/sensibo.py @andrey-git
|
||||
homeassistant/components/cover/brunt.py @eavanvalkenburg
|
||||
homeassistant/components/cover/group.py @cdce8p
|
||||
homeassistant/components/cover/template.py @PhracturedBlue
|
||||
homeassistant/components/device_tracker/asuswrt.py @kennedyshead
|
||||
homeassistant/components/device_tracker/automatic.py @armills
|
||||
homeassistant/components/device_tracker/huawei_router.py @abmantis
|
||||
homeassistant/components/device_tracker/quantum_gateway.py @cisasteelersfan
|
||||
homeassistant/components/device_tracker/tile.py @bachya
|
||||
homeassistant/components/device_tracker/bt_smarthub.py @jxwolstenholme
|
||||
homeassistant/components/history_graph.py @andrey-git
|
||||
homeassistant/components/influx.py @fabaff
|
||||
homeassistant/components/light/lifx_legacy.py @amelchio
|
||||
homeassistant/components/light/tplink.py @rytilahti
|
||||
homeassistant/components/light/yeelight.py @rytilahti
|
||||
homeassistant/components/light/yeelightsunflower.py @lindsaymarkward
|
||||
homeassistant/components/lock/nello.py @pschmitt
|
||||
homeassistant/components/lock/nuki.py @pschmitt
|
||||
homeassistant/components/media_player/emby.py @mezz64
|
||||
@@ -86,6 +90,7 @@ homeassistant/components/notify/mastodon.py @fabaff
|
||||
homeassistant/components/notify/smtp.py @fabaff
|
||||
homeassistant/components/notify/syslog.py @fabaff
|
||||
homeassistant/components/notify/xmpp.py @fabaff
|
||||
homeassistant/components/notify/yessssms.py @flowolf
|
||||
homeassistant/components/plant.py @ChristianKuehnel
|
||||
homeassistant/components/scene/lifx_cloud.py @amelchio
|
||||
homeassistant/components/sensor/airvisual.py @bachya
|
||||
@@ -97,6 +102,7 @@ homeassistant/components/sensor/darksky.py @fabaff
|
||||
homeassistant/components/sensor/file.py @fabaff
|
||||
homeassistant/components/sensor/filter.py @dgomes
|
||||
homeassistant/components/sensor/fixer.py @fabaff
|
||||
homeassistant/components/sensor/flunearyou.py.py @bachya
|
||||
homeassistant/components/sensor/gearbest.py @HerrHofrat
|
||||
homeassistant/components/sensor/gitter.py @fabaff
|
||||
homeassistant/components/sensor/glances.py @fabaff
|
||||
@@ -104,7 +110,6 @@ homeassistant/components/sensor/gpsd.py @fabaff
|
||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||
homeassistant/components/sensor/jewish_calendar.py @tsvi
|
||||
homeassistant/components/sensor/linux_battery.py @fabaff
|
||||
homeassistant/components/sensor/luftdaten.py @fabaff
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/sensor/min_max.py @fabaff
|
||||
homeassistant/components/sensor/moon.py @fabaff
|
||||
@@ -116,6 +121,7 @@ homeassistant/components/sensor/pvoutput.py @fabaff
|
||||
homeassistant/components/sensor/qnap.py @colinodell
|
||||
homeassistant/components/sensor/scrape.py @fabaff
|
||||
homeassistant/components/sensor/serial.py @fabaff
|
||||
homeassistant/components/sensor/seventeentrack.py @bachya
|
||||
homeassistant/components/sensor/shodan.py @fabaff
|
||||
homeassistant/components/sensor/sma.py @kellerza
|
||||
homeassistant/components/sensor/sql.py @dgomes
|
||||
@@ -184,6 +190,8 @@ homeassistant/components/*/konnected.py @heythisisnate
|
||||
# L
|
||||
homeassistant/components/lifx.py @amelchio
|
||||
homeassistant/components/*/lifx.py @amelchio
|
||||
homeassistant/components/luftdaten/* @fabaff
|
||||
homeassistant/components/*/luftdaten.py @fabaff
|
||||
|
||||
# M
|
||||
homeassistant/components/matrix.py @tinloaf
|
||||
@@ -234,6 +242,10 @@ homeassistant/components/*/upcloud.py @scop
|
||||
homeassistant/components/velux.py @Julius2342
|
||||
homeassistant/components/*/velux.py @Julius2342
|
||||
|
||||
# W
|
||||
homeassistant/components/wemo.py @sqldiablo
|
||||
homeassistant/components/*/wemo.py @sqldiablo
|
||||
|
||||
# X
|
||||
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
|
||||
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
|
||||
|
||||
@@ -11,6 +11,7 @@ LABEL maintainer="Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
|
||||
#ENV INSTALL_FFMPEG no
|
||||
#ENV INSTALL_LIBCEC no
|
||||
#ENV INSTALL_SSOCR no
|
||||
#ENV INSTALL_DLIB no
|
||||
#ENV INSTALL_IPERF3 no
|
||||
|
||||
VOLUME /config
|
||||
@@ -27,7 +28,7 @@ COPY requirements_all.txt requirements_all.txt
|
||||
# Uninstall enum34 because some dependencies install it but breaks Python 3.4+.
|
||||
# See PR #8103 for more info.
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
|
||||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet cython
|
||||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet cython tensorflow
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.core import callback, HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import auth_store, models
|
||||
from .const import GROUP_ID_ADMIN
|
||||
from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule
|
||||
from .providers import auth_provider_from_config, AuthProvider, LoginFlow
|
||||
|
||||
@@ -117,6 +118,10 @@ class AuthManager:
|
||||
"""Retrieve a user."""
|
||||
return await self._store.async_get_user(user_id)
|
||||
|
||||
async def async_get_group(self, group_id: str) -> Optional[models.Group]:
|
||||
"""Retrieve all groups."""
|
||||
return await self._store.async_get_group(group_id)
|
||||
|
||||
async def async_get_user_by_credentials(
|
||||
self, credentials: models.Credentials) -> Optional[models.User]:
|
||||
"""Get a user by credential, return None if not found."""
|
||||
@@ -133,7 +138,7 @@ class AuthManager:
|
||||
name=name,
|
||||
system_generated=True,
|
||||
is_active=True,
|
||||
groups=[],
|
||||
group_ids=[],
|
||||
)
|
||||
|
||||
self.hass.bus.async_fire(EVENT_USER_ADDED, {
|
||||
@@ -144,11 +149,10 @@ class AuthManager:
|
||||
|
||||
async def async_create_user(self, name: str) -> models.User:
|
||||
"""Create a user."""
|
||||
group = (await self._store.async_get_groups())[0]
|
||||
kwargs = {
|
||||
'name': name,
|
||||
'is_active': True,
|
||||
'groups': [group]
|
||||
'group_ids': [GROUP_ID_ADMIN]
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if await self._user_should_be_owner():
|
||||
@@ -342,7 +346,6 @@ class AuthManager:
|
||||
"""Create a new access token."""
|
||||
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
now = dt_util.utcnow()
|
||||
return jwt.encode({
|
||||
'iss': refresh_token.id,
|
||||
|
||||
@@ -10,11 +10,14 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import models
|
||||
from .permissions import DEFAULT_POLICY
|
||||
from .const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY
|
||||
from .permissions import system_policies
|
||||
from .permissions.types import PolicyType # noqa: F401
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth'
|
||||
INITIAL_GROUP_NAME = 'All Access'
|
||||
GROUP_NAME_ADMIN = 'Administrators'
|
||||
GROUP_NAME_READ_ONLY = 'Read Only'
|
||||
|
||||
|
||||
class AuthStore:
|
||||
@@ -42,6 +45,14 @@ class AuthStore:
|
||||
|
||||
return list(self._groups.values())
|
||||
|
||||
async def async_get_group(self, group_id: str) -> Optional[models.Group]:
|
||||
"""Retrieve all users."""
|
||||
if self._groups is None:
|
||||
await self._async_load()
|
||||
assert self._groups is not None
|
||||
|
||||
return self._groups.get(group_id)
|
||||
|
||||
async def async_get_users(self) -> List[models.User]:
|
||||
"""Retrieve all users."""
|
||||
if self._users is None:
|
||||
@@ -63,7 +74,7 @@ class AuthStore:
|
||||
is_active: Optional[bool] = None,
|
||||
system_generated: Optional[bool] = None,
|
||||
credentials: Optional[models.Credentials] = None,
|
||||
groups: Optional[List[models.Group]] = None) -> models.User:
|
||||
group_ids: Optional[List[str]] = None) -> models.User:
|
||||
"""Create a new user."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
@@ -71,11 +82,18 @@ class AuthStore:
|
||||
assert self._users is not None
|
||||
assert self._groups is not None
|
||||
|
||||
groups = []
|
||||
for group_id in (group_ids or []):
|
||||
group = self._groups.get(group_id)
|
||||
if group is None:
|
||||
raise ValueError('Invalid group specified {}'.format(group_id))
|
||||
groups.append(group)
|
||||
|
||||
kwargs = {
|
||||
'name': name,
|
||||
# Until we get group management, we just put everyone in the
|
||||
# same group.
|
||||
'groups': groups or [],
|
||||
'groups': groups,
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if is_owner is not None:
|
||||
@@ -238,38 +256,98 @@ class AuthStore:
|
||||
users = OrderedDict() # type: Dict[str, models.User]
|
||||
groups = OrderedDict() # type: Dict[str, models.Group]
|
||||
|
||||
# When creating objects we mention each attribute explicetely. This
|
||||
# Soft-migrating data as we load. We are going to make sure we have a
|
||||
# read only group and an admin group. There are two states that we can
|
||||
# migrate from:
|
||||
# 1. Data from a recent version which has a single group without policy
|
||||
# 2. Data from old version which has no groups
|
||||
has_admin_group = False
|
||||
has_read_only_group = False
|
||||
group_without_policy = None
|
||||
|
||||
# When creating objects we mention each attribute explicitly. This
|
||||
# prevents crashing if user rolls back HA version after a new property
|
||||
# was added.
|
||||
|
||||
for group_dict in data.get('groups', []):
|
||||
policy = None # type: Optional[PolicyType]
|
||||
|
||||
if group_dict['id'] == GROUP_ID_ADMIN:
|
||||
has_admin_group = True
|
||||
|
||||
name = GROUP_NAME_ADMIN
|
||||
policy = system_policies.ADMIN_POLICY
|
||||
system_generated = True
|
||||
|
||||
elif group_dict['id'] == GROUP_ID_READ_ONLY:
|
||||
has_read_only_group = True
|
||||
|
||||
name = GROUP_NAME_READ_ONLY
|
||||
policy = system_policies.READ_ONLY_POLICY
|
||||
system_generated = True
|
||||
|
||||
else:
|
||||
name = group_dict['name']
|
||||
policy = group_dict.get('policy')
|
||||
system_generated = False
|
||||
|
||||
# We don't want groups without a policy that are not system groups
|
||||
# This is part of migrating from state 1
|
||||
if policy is None:
|
||||
group_without_policy = group_dict['id']
|
||||
continue
|
||||
|
||||
groups[group_dict['id']] = models.Group(
|
||||
name=group_dict['name'],
|
||||
id=group_dict['id'],
|
||||
policy=group_dict.get('policy', DEFAULT_POLICY),
|
||||
name=name,
|
||||
policy=policy,
|
||||
system_generated=system_generated,
|
||||
)
|
||||
|
||||
migrate_group = None
|
||||
# If there are no groups, add all existing users to the admin group.
|
||||
# This is part of migrating from state 2
|
||||
migrate_users_to_admin_group = (not groups and
|
||||
group_without_policy is None)
|
||||
|
||||
if not groups:
|
||||
migrate_group = models.Group(
|
||||
name=INITIAL_GROUP_NAME,
|
||||
policy=DEFAULT_POLICY
|
||||
)
|
||||
groups[migrate_group.id] = migrate_group
|
||||
# If we find a no_policy_group, we need to migrate all users to the
|
||||
# admin group. We only do this if there are no other groups, as is
|
||||
# the expected state. If not expected state, not marking people admin.
|
||||
# This is part of migrating from state 1
|
||||
if groups and group_without_policy is not None:
|
||||
group_without_policy = None
|
||||
|
||||
# This is part of migrating from state 1 and 2
|
||||
if not has_admin_group:
|
||||
admin_group = _system_admin_group()
|
||||
groups[admin_group.id] = admin_group
|
||||
|
||||
# This is part of migrating from state 1 and 2
|
||||
if not has_read_only_group:
|
||||
read_only_group = _system_read_only_group()
|
||||
groups[read_only_group.id] = read_only_group
|
||||
|
||||
for user_dict in data['users']:
|
||||
# Collect the users group.
|
||||
user_groups = []
|
||||
for group_id in user_dict.get('group_ids', []):
|
||||
# This is part of migrating from state 1
|
||||
if group_id == group_without_policy:
|
||||
group_id = GROUP_ID_ADMIN
|
||||
user_groups.append(groups[group_id])
|
||||
|
||||
# This is part of migrating from state 2
|
||||
if (not user_dict['system_generated'] and
|
||||
migrate_users_to_admin_group):
|
||||
user_groups.append(groups[GROUP_ID_ADMIN])
|
||||
|
||||
users[user_dict['id']] = models.User(
|
||||
name=user_dict['name'],
|
||||
groups=[groups[group_id] for group_id
|
||||
in user_dict.get('group_ids', [])],
|
||||
groups=user_groups,
|
||||
id=user_dict['id'],
|
||||
is_owner=user_dict['is_owner'],
|
||||
is_active=user_dict['is_active'],
|
||||
system_generated=user_dict['system_generated'],
|
||||
)
|
||||
if migrate_group is not None and not user_dict['system_generated']:
|
||||
users[user_dict['id']].groups = [migrate_group]
|
||||
|
||||
for cred_dict in data['credentials']:
|
||||
users[cred_dict['user_id']].credentials.append(models.Credentials(
|
||||
@@ -356,11 +434,11 @@ class AuthStore:
|
||||
groups = []
|
||||
for group in self._groups.values():
|
||||
g_dict = {
|
||||
'name': group.name,
|
||||
'id': group.id,
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if group.policy is not DEFAULT_POLICY:
|
||||
if group.id not in (GROUP_ID_READ_ONLY, GROUP_ID_ADMIN):
|
||||
g_dict['name'] = group.name
|
||||
g_dict['policy'] = group.policy
|
||||
|
||||
groups.append(g_dict)
|
||||
@@ -410,13 +488,29 @@ class AuthStore:
|
||||
"""Set default values for auth store."""
|
||||
self._users = OrderedDict() # type: Dict[str, models.User]
|
||||
|
||||
# Add default group
|
||||
all_access_group = models.Group(
|
||||
name=INITIAL_GROUP_NAME,
|
||||
policy=DEFAULT_POLICY,
|
||||
)
|
||||
|
||||
groups = OrderedDict() # type: Dict[str, models.Group]
|
||||
groups[all_access_group.id] = all_access_group
|
||||
|
||||
admin_group = _system_admin_group()
|
||||
groups[admin_group.id] = admin_group
|
||||
read_only_group = _system_read_only_group()
|
||||
groups[read_only_group.id] = read_only_group
|
||||
self._groups = groups
|
||||
|
||||
|
||||
def _system_admin_group() -> models.Group:
|
||||
"""Create system admin group."""
|
||||
return models.Group(
|
||||
name=GROUP_NAME_ADMIN,
|
||||
id=GROUP_ID_ADMIN,
|
||||
policy=system_policies.ADMIN_POLICY,
|
||||
system_generated=True,
|
||||
)
|
||||
|
||||
|
||||
def _system_read_only_group() -> models.Group:
|
||||
"""Create read only group."""
|
||||
return models.Group(
|
||||
name=GROUP_NAME_READ_ONLY,
|
||||
id=GROUP_ID_READ_ONLY,
|
||||
policy=system_policies.READ_ONLY_POLICY,
|
||||
system_generated=True,
|
||||
)
|
||||
|
||||
@@ -3,3 +3,6 @@ from datetime import timedelta
|
||||
|
||||
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
||||
MFA_SESSION_EXPIRATION = timedelta(minutes=5)
|
||||
|
||||
GROUP_ID_ADMIN = 'system-admin'
|
||||
GROUP_ID_READ_ONLY = 'system-read-only'
|
||||
|
||||
@@ -104,7 +104,7 @@ class SetupFlow(data_entry_flow.FlowHandler):
|
||||
-> Dict[str, Any]:
|
||||
"""Handle the first step of setup flow.
|
||||
|
||||
Return self.async_show_form(step_id='init') if user_input == None.
|
||||
Return self.async_show_form(step_id='init') if user_input is None.
|
||||
Return self.async_create_entry(data={'result': result}) if finish.
|
||||
"""
|
||||
errors = {} # type: Dict[str, str]
|
||||
|
||||
@@ -176,7 +176,7 @@ class TotpSetupFlow(SetupFlow):
|
||||
-> Dict[str, Any]:
|
||||
"""Handle the first step of setup flow.
|
||||
|
||||
Return self.async_show_form(step_id='init') if user_input == None.
|
||||
Return self.async_show_form(step_id='init') if user_input is None.
|
||||
Return self.async_create_entry(data={'result': result}) if finish.
|
||||
"""
|
||||
import pyotp
|
||||
|
||||
@@ -22,6 +22,7 @@ class Group:
|
||||
name = attr.ib(type=str) # type: Optional[str]
|
||||
policy = attr.ib(type=perm_mdl.PolicyType)
|
||||
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
|
||||
system_generated = attr.ib(type=bool, default=False)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
"""Permissions for Home Assistant."""
|
||||
from typing import ( # noqa: F401
|
||||
cast, Any, Callable, Dict, List, Mapping, Set, Tuple, Union)
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import State
|
||||
|
||||
CategoryType = Union[Mapping[str, 'CategoryType'], bool, None]
|
||||
PolicyType = Mapping[str, CategoryType]
|
||||
|
||||
|
||||
# Default policy if group has no policy applied.
|
||||
DEFAULT_POLICY = {
|
||||
"entities": True
|
||||
} # type: PolicyType
|
||||
|
||||
CAT_ENTITIES = 'entities'
|
||||
ENTITY_DOMAINS = 'domains'
|
||||
ENTITY_ENTITY_IDS = 'entity_ids'
|
||||
|
||||
VALUES_SCHEMA = vol.Any(True, vol.Schema({
|
||||
str: True
|
||||
}))
|
||||
|
||||
ENTITY_POLICY_SCHEMA = vol.Any(True, vol.Schema({
|
||||
vol.Optional(ENTITY_DOMAINS): VALUES_SCHEMA,
|
||||
vol.Optional(ENTITY_ENTITY_IDS): VALUES_SCHEMA,
|
||||
}))
|
||||
|
||||
POLICY_SCHEMA = vol.Schema({
|
||||
vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA
|
||||
})
|
||||
|
||||
|
||||
class AbstractPermissions:
|
||||
"""Default permissions class."""
|
||||
|
||||
def check_entity(self, entity_id: str, *keys: str) -> bool:
|
||||
"""Test if we can access entity."""
|
||||
raise NotImplementedError
|
||||
|
||||
def filter_states(self, states: List[State]) -> List[State]:
|
||||
"""Filter a list of states for what the user is allowed to see."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PolicyPermissions(AbstractPermissions):
|
||||
"""Handle permissions."""
|
||||
|
||||
def __init__(self, policy: PolicyType) -> None:
|
||||
"""Initialize the permission class."""
|
||||
self._policy = policy
|
||||
self._compiled = {} # type: Dict[str, Callable[..., bool]]
|
||||
|
||||
def check_entity(self, entity_id: str, *keys: str) -> bool:
|
||||
"""Test if we can access entity."""
|
||||
func = self._policy_func(CAT_ENTITIES, _compile_entities)
|
||||
return func(entity_id, keys)
|
||||
|
||||
def filter_states(self, states: List[State]) -> List[State]:
|
||||
"""Filter a list of states for what the user is allowed to see."""
|
||||
func = self._policy_func(CAT_ENTITIES, _compile_entities)
|
||||
keys = ('read',)
|
||||
return [entity for entity in states if func(entity.entity_id, keys)]
|
||||
|
||||
def _policy_func(self, category: str,
|
||||
compile_func: Callable[[CategoryType], Callable]) \
|
||||
-> Callable[..., bool]:
|
||||
"""Get a policy function."""
|
||||
func = self._compiled.get(category)
|
||||
|
||||
if func:
|
||||
return func
|
||||
|
||||
func = self._compiled[category] = compile_func(
|
||||
self._policy.get(category))
|
||||
return func
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Equals check."""
|
||||
# pylint: disable=protected-access
|
||||
return (isinstance(other, PolicyPermissions) and
|
||||
other._policy == self._policy)
|
||||
|
||||
|
||||
class _OwnerPermissions(AbstractPermissions):
|
||||
"""Owner permissions."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
def check_entity(self, entity_id: str, *keys: str) -> bool:
|
||||
"""Test if we can access entity."""
|
||||
return True
|
||||
|
||||
def filter_states(self, states: List[State]) -> List[State]:
|
||||
"""Filter a list of states for what the user is allowed to see."""
|
||||
return states
|
||||
|
||||
|
||||
OwnerPermissions = _OwnerPermissions() # pylint: disable=invalid-name
|
||||
|
||||
|
||||
def _compile_entities(policy: CategoryType) \
|
||||
-> Callable[[str, Tuple[str]], bool]:
|
||||
"""Compile policy into a function that tests policy."""
|
||||
# None, Empty Dict, False
|
||||
if not policy:
|
||||
def apply_policy_deny_all(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Decline all."""
|
||||
return False
|
||||
|
||||
return apply_policy_deny_all
|
||||
|
||||
if policy is True:
|
||||
def apply_policy_allow_all(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Approve all."""
|
||||
return True
|
||||
|
||||
return apply_policy_allow_all
|
||||
|
||||
assert isinstance(policy, dict)
|
||||
|
||||
domains = policy.get(ENTITY_DOMAINS)
|
||||
entity_ids = policy.get(ENTITY_ENTITY_IDS)
|
||||
|
||||
funcs = [] # type: List[Callable[[str, Tuple[str]], Union[None, bool]]]
|
||||
|
||||
# The order of these functions matter. The more precise are at the top.
|
||||
# If a function returns None, they cannot handle it.
|
||||
# If a function returns a boolean, that's the result to return.
|
||||
|
||||
# Setting entity_ids to a boolean is final decision for permissions
|
||||
# So return right away.
|
||||
if isinstance(entity_ids, bool):
|
||||
def apply_entity_id_policy(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Test if allowed entity_id."""
|
||||
return entity_ids # type: ignore
|
||||
|
||||
return apply_entity_id_policy
|
||||
|
||||
if entity_ids is not None:
|
||||
def allowed_entity_id(entity_id: str, keys: Tuple[str]) \
|
||||
-> Union[None, bool]:
|
||||
"""Test if allowed entity_id."""
|
||||
return entity_ids.get(entity_id) # type: ignore
|
||||
|
||||
funcs.append(allowed_entity_id)
|
||||
|
||||
if isinstance(domains, bool):
|
||||
def allowed_domain(entity_id: str, keys: Tuple[str]) \
|
||||
-> Union[None, bool]:
|
||||
"""Test if allowed domain."""
|
||||
return domains
|
||||
|
||||
funcs.append(allowed_domain)
|
||||
|
||||
elif domains is not None:
|
||||
def allowed_domain(entity_id: str, keys: Tuple[str]) \
|
||||
-> Union[None, bool]:
|
||||
"""Test if allowed domain."""
|
||||
domain = entity_id.split(".", 1)[0]
|
||||
return domains.get(domain) # type: ignore
|
||||
|
||||
funcs.append(allowed_domain)
|
||||
|
||||
# Can happen if no valid subcategories specified
|
||||
if not funcs:
|
||||
def apply_policy_deny_all_2(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Decline all."""
|
||||
return False
|
||||
|
||||
return apply_policy_deny_all_2
|
||||
|
||||
if len(funcs) == 1:
|
||||
func = funcs[0]
|
||||
|
||||
def apply_policy_func(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Apply a single policy function."""
|
||||
return func(entity_id, keys) is True
|
||||
|
||||
return apply_policy_func
|
||||
|
||||
def apply_policy_funcs(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Apply several policy functions."""
|
||||
for func in funcs:
|
||||
result = func(entity_id, keys)
|
||||
if result is not None:
|
||||
return result
|
||||
return False
|
||||
|
||||
return apply_policy_funcs
|
||||
|
||||
|
||||
def merge_policies(policies: List[PolicyType]) -> PolicyType:
|
||||
"""Merge policies."""
|
||||
new_policy = {} # type: Dict[str, CategoryType]
|
||||
seen = set() # type: Set[str]
|
||||
for policy in policies:
|
||||
for category in policy:
|
||||
if category in seen:
|
||||
continue
|
||||
seen.add(category)
|
||||
new_policy[category] = _merge_policies([
|
||||
policy.get(category) for policy in policies])
|
||||
cast(PolicyType, new_policy)
|
||||
return new_policy
|
||||
|
||||
|
||||
def _merge_policies(sources: List[CategoryType]) -> CategoryType:
|
||||
"""Merge a policy."""
|
||||
# When merging policies, the most permissive wins.
|
||||
# This means we order it like this:
|
||||
# True > Dict > None
|
||||
#
|
||||
# True: allow everything
|
||||
# Dict: specify more granular permissions
|
||||
# None: no opinion
|
||||
#
|
||||
# If there are multiple sources with a dict as policy, we recursively
|
||||
# merge each key in the source.
|
||||
|
||||
policy = None # type: CategoryType
|
||||
seen = set() # type: Set[str]
|
||||
for source in sources:
|
||||
if source is None:
|
||||
continue
|
||||
|
||||
# A source that's True will always win. Shortcut return.
|
||||
if source is True:
|
||||
return True
|
||||
|
||||
assert isinstance(source, dict)
|
||||
|
||||
if policy is None:
|
||||
policy = {}
|
||||
|
||||
assert isinstance(policy, dict)
|
||||
|
||||
for key in source:
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
|
||||
key_sources = []
|
||||
for src in sources:
|
||||
if isinstance(src, dict):
|
||||
key_sources.append(src.get(key))
|
||||
|
||||
policy[key] = _merge_policies(key_sources)
|
||||
|
||||
return policy
|
||||
90
homeassistant/auth/permissions/__init__.py
Normal file
90
homeassistant/auth/permissions/__init__.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Permissions for Home Assistant."""
|
||||
import logging
|
||||
from typing import ( # noqa: F401
|
||||
cast, Any, Callable, Dict, List, Mapping, Set, Tuple, Union)
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import State
|
||||
|
||||
from .const import CAT_ENTITIES
|
||||
from .types import CategoryType, PolicyType
|
||||
from .entities import ENTITY_POLICY_SCHEMA, compile_entities
|
||||
from .merge import merge_policies # noqa
|
||||
|
||||
POLICY_SCHEMA = vol.Schema({
|
||||
vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AbstractPermissions:
|
||||
"""Default permissions class."""
|
||||
|
||||
def check_entity(self, entity_id: str, key: str) -> bool:
|
||||
"""Test if we can access entity."""
|
||||
raise NotImplementedError
|
||||
|
||||
def filter_states(self, states: List[State]) -> List[State]:
|
||||
"""Filter a list of states for what the user is allowed to see."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PolicyPermissions(AbstractPermissions):
|
||||
"""Handle permissions."""
|
||||
|
||||
def __init__(self, policy: PolicyType) -> None:
|
||||
"""Initialize the permission class."""
|
||||
self._policy = policy
|
||||
self._compiled = {} # type: Dict[str, Callable[..., bool]]
|
||||
|
||||
def check_entity(self, entity_id: str, key: str) -> bool:
|
||||
"""Test if we can access entity."""
|
||||
func = self._policy_func(CAT_ENTITIES, compile_entities)
|
||||
return func(entity_id, (key,))
|
||||
|
||||
def filter_states(self, states: List[State]) -> List[State]:
|
||||
"""Filter a list of states for what the user is allowed to see."""
|
||||
func = self._policy_func(CAT_ENTITIES, compile_entities)
|
||||
keys = ('read',)
|
||||
return [entity for entity in states if func(entity.entity_id, keys)]
|
||||
|
||||
def _policy_func(self, category: str,
|
||||
compile_func: Callable[[CategoryType], Callable]) \
|
||||
-> Callable[..., bool]:
|
||||
"""Get a policy function."""
|
||||
func = self._compiled.get(category)
|
||||
|
||||
if func:
|
||||
return func
|
||||
|
||||
func = self._compiled[category] = compile_func(
|
||||
self._policy.get(category))
|
||||
|
||||
_LOGGER.debug("Compiled %s func: %s", category, func)
|
||||
|
||||
return func
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Equals check."""
|
||||
# pylint: disable=protected-access
|
||||
return (isinstance(other, PolicyPermissions) and
|
||||
other._policy == self._policy)
|
||||
|
||||
|
||||
class _OwnerPermissions(AbstractPermissions):
|
||||
"""Owner permissions."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
def check_entity(self, entity_id: str, key: str) -> bool:
|
||||
"""Test if we can access entity."""
|
||||
return True
|
||||
|
||||
def filter_states(self, states: List[State]) -> List[State]:
|
||||
"""Filter a list of states for what the user is allowed to see."""
|
||||
return states
|
||||
|
||||
|
||||
OwnerPermissions = _OwnerPermissions() # pylint: disable=invalid-name
|
||||
7
homeassistant/auth/permissions/const.py
Normal file
7
homeassistant/auth/permissions/const.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Permission constants."""
|
||||
CAT_ENTITIES = 'entities'
|
||||
SUBCAT_ALL = 'all'
|
||||
|
||||
POLICY_READ = 'read'
|
||||
POLICY_CONTROL = 'control'
|
||||
POLICY_EDIT = 'edit'
|
||||
145
homeassistant/auth/permissions/entities.py
Normal file
145
homeassistant/auth/permissions/entities.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""Entity permissions."""
|
||||
from functools import wraps
|
||||
from typing import ( # noqa: F401
|
||||
Callable, Dict, List, Tuple, Union)
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from .const import SUBCAT_ALL, POLICY_READ, POLICY_CONTROL, POLICY_EDIT
|
||||
from .types import CategoryType, ValueType
|
||||
|
||||
SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({
|
||||
vol.Optional(POLICY_READ): True,
|
||||
vol.Optional(POLICY_CONTROL): True,
|
||||
vol.Optional(POLICY_EDIT): True,
|
||||
}))
|
||||
|
||||
ENTITY_DOMAINS = 'domains'
|
||||
ENTITY_ENTITY_IDS = 'entity_ids'
|
||||
|
||||
ENTITY_VALUES_SCHEMA = vol.Any(True, vol.Schema({
|
||||
str: SINGLE_ENTITY_SCHEMA
|
||||
}))
|
||||
|
||||
ENTITY_POLICY_SCHEMA = vol.Any(True, vol.Schema({
|
||||
vol.Optional(SUBCAT_ALL): SINGLE_ENTITY_SCHEMA,
|
||||
vol.Optional(ENTITY_DOMAINS): ENTITY_VALUES_SCHEMA,
|
||||
vol.Optional(ENTITY_ENTITY_IDS): ENTITY_VALUES_SCHEMA,
|
||||
}))
|
||||
|
||||
|
||||
def _entity_allowed(schema: ValueType, keys: Tuple[str]) \
|
||||
-> Union[bool, None]:
|
||||
"""Test if an entity is allowed based on the keys."""
|
||||
if schema is None or isinstance(schema, bool):
|
||||
return schema
|
||||
assert isinstance(schema, dict)
|
||||
return schema.get(keys[0])
|
||||
|
||||
|
||||
def compile_entities(policy: CategoryType) \
|
||||
-> Callable[[str, Tuple[str]], bool]:
|
||||
"""Compile policy into a function that tests policy."""
|
||||
# None, Empty Dict, False
|
||||
if not policy:
|
||||
def apply_policy_deny_all(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Decline all."""
|
||||
return False
|
||||
|
||||
return apply_policy_deny_all
|
||||
|
||||
if policy is True:
|
||||
def apply_policy_allow_all(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Approve all."""
|
||||
return True
|
||||
|
||||
return apply_policy_allow_all
|
||||
|
||||
assert isinstance(policy, dict)
|
||||
|
||||
domains = policy.get(ENTITY_DOMAINS)
|
||||
entity_ids = policy.get(ENTITY_ENTITY_IDS)
|
||||
all_entities = policy.get(SUBCAT_ALL)
|
||||
|
||||
funcs = [] # type: List[Callable[[str, Tuple[str]], Union[None, bool]]]
|
||||
|
||||
# The order of these functions matter. The more precise are at the top.
|
||||
# If a function returns None, they cannot handle it.
|
||||
# If a function returns a boolean, that's the result to return.
|
||||
|
||||
# Setting entity_ids to a boolean is final decision for permissions
|
||||
# So return right away.
|
||||
if isinstance(entity_ids, bool):
|
||||
def allowed_entity_id_bool(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Test if allowed entity_id."""
|
||||
return entity_ids # type: ignore
|
||||
|
||||
return allowed_entity_id_bool
|
||||
|
||||
if entity_ids is not None:
|
||||
def allowed_entity_id_dict(entity_id: str, keys: Tuple[str]) \
|
||||
-> Union[None, bool]:
|
||||
"""Test if allowed entity_id."""
|
||||
return _entity_allowed(
|
||||
entity_ids.get(entity_id), keys) # type: ignore
|
||||
|
||||
funcs.append(allowed_entity_id_dict)
|
||||
|
||||
if isinstance(domains, bool):
|
||||
def allowed_domain_bool(entity_id: str, keys: Tuple[str]) \
|
||||
-> Union[None, bool]:
|
||||
"""Test if allowed domain."""
|
||||
return domains
|
||||
|
||||
funcs.append(allowed_domain_bool)
|
||||
|
||||
elif domains is not None:
|
||||
def allowed_domain_dict(entity_id: str, keys: Tuple[str]) \
|
||||
-> Union[None, bool]:
|
||||
"""Test if allowed domain."""
|
||||
domain = entity_id.split(".", 1)[0]
|
||||
return _entity_allowed(domains.get(domain), keys) # type: ignore
|
||||
|
||||
funcs.append(allowed_domain_dict)
|
||||
|
||||
if isinstance(all_entities, bool):
|
||||
def allowed_all_entities_bool(entity_id: str, keys: Tuple[str]) \
|
||||
-> Union[None, bool]:
|
||||
"""Test if allowed domain."""
|
||||
return all_entities
|
||||
funcs.append(allowed_all_entities_bool)
|
||||
|
||||
elif all_entities is not None:
|
||||
def allowed_all_entities_dict(entity_id: str, keys: Tuple[str]) \
|
||||
-> Union[None, bool]:
|
||||
"""Test if allowed domain."""
|
||||
return _entity_allowed(all_entities, keys)
|
||||
funcs.append(allowed_all_entities_dict)
|
||||
|
||||
# Can happen if no valid subcategories specified
|
||||
if not funcs:
|
||||
def apply_policy_deny_all_2(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Decline all."""
|
||||
return False
|
||||
|
||||
return apply_policy_deny_all_2
|
||||
|
||||
if len(funcs) == 1:
|
||||
func = funcs[0]
|
||||
|
||||
@wraps(func)
|
||||
def apply_policy_func(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Apply a single policy function."""
|
||||
return func(entity_id, keys) is True
|
||||
|
||||
return apply_policy_func
|
||||
|
||||
def apply_policy_funcs(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Apply several policy functions."""
|
||||
for func in funcs:
|
||||
result = func(entity_id, keys)
|
||||
if result is not None:
|
||||
return result
|
||||
return False
|
||||
|
||||
return apply_policy_funcs
|
||||
65
homeassistant/auth/permissions/merge.py
Normal file
65
homeassistant/auth/permissions/merge.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""Merging of policies."""
|
||||
from typing import ( # noqa: F401
|
||||
cast, Dict, List, Set)
|
||||
|
||||
from .types import PolicyType, CategoryType
|
||||
|
||||
|
||||
def merge_policies(policies: List[PolicyType]) -> PolicyType:
|
||||
"""Merge policies."""
|
||||
new_policy = {} # type: Dict[str, CategoryType]
|
||||
seen = set() # type: Set[str]
|
||||
for policy in policies:
|
||||
for category in policy:
|
||||
if category in seen:
|
||||
continue
|
||||
seen.add(category)
|
||||
new_policy[category] = _merge_policies([
|
||||
policy.get(category) for policy in policies])
|
||||
cast(PolicyType, new_policy)
|
||||
return new_policy
|
||||
|
||||
|
||||
def _merge_policies(sources: List[CategoryType]) -> CategoryType:
|
||||
"""Merge a policy."""
|
||||
# When merging policies, the most permissive wins.
|
||||
# This means we order it like this:
|
||||
# True > Dict > None
|
||||
#
|
||||
# True: allow everything
|
||||
# Dict: specify more granular permissions
|
||||
# None: no opinion
|
||||
#
|
||||
# If there are multiple sources with a dict as policy, we recursively
|
||||
# merge each key in the source.
|
||||
|
||||
policy = None # type: CategoryType
|
||||
seen = set() # type: Set[str]
|
||||
for source in sources:
|
||||
if source is None:
|
||||
continue
|
||||
|
||||
# A source that's True will always win. Shortcut return.
|
||||
if source is True:
|
||||
return True
|
||||
|
||||
assert isinstance(source, dict)
|
||||
|
||||
if policy is None:
|
||||
policy = cast(CategoryType, {})
|
||||
|
||||
assert isinstance(policy, dict)
|
||||
|
||||
for key in source:
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
|
||||
key_sources = []
|
||||
for src in sources:
|
||||
if isinstance(src, dict):
|
||||
key_sources.append(src.get(key))
|
||||
|
||||
policy[key] = _merge_policies(key_sources)
|
||||
|
||||
return policy
|
||||
14
homeassistant/auth/permissions/system_policies.py
Normal file
14
homeassistant/auth/permissions/system_policies.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""System policies."""
|
||||
from .const import CAT_ENTITIES, SUBCAT_ALL, POLICY_READ
|
||||
|
||||
ADMIN_POLICY = {
|
||||
CAT_ENTITIES: True,
|
||||
}
|
||||
|
||||
READ_ONLY_POLICY = {
|
||||
CAT_ENTITIES: {
|
||||
SUBCAT_ALL: {
|
||||
POLICY_READ: True
|
||||
}
|
||||
}
|
||||
}
|
||||
31
homeassistant/auth/permissions/types.py
Normal file
31
homeassistant/auth/permissions/types.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Common code for permissions."""
|
||||
from typing import ( # noqa: F401
|
||||
Mapping, Union, Any)
|
||||
|
||||
# MyPy doesn't support recursion yet. So writing it out as far as we need.
|
||||
|
||||
ValueType = Union[
|
||||
# Example: entities.all = { read: true, control: true }
|
||||
Mapping[str, bool],
|
||||
bool,
|
||||
None
|
||||
]
|
||||
|
||||
SubCategoryType = Union[
|
||||
# Example: entities.domains = { light: … }
|
||||
Mapping[str, ValueType],
|
||||
bool,
|
||||
None
|
||||
]
|
||||
|
||||
CategoryType = Union[
|
||||
# Example: entities.domains
|
||||
Mapping[str, SubCategoryType],
|
||||
# Example: entities.all
|
||||
Mapping[str, ValueType],
|
||||
bool,
|
||||
None
|
||||
]
|
||||
|
||||
# Example: { entities: … }
|
||||
PolicyType = Mapping[str, CategoryType]
|
||||
@@ -179,7 +179,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
-> Dict[str, Any]:
|
||||
"""Handle the first step of login flow.
|
||||
|
||||
Return self.async_show_form(step_id='init') if user_input == None.
|
||||
Return self.async_show_form(step_id='init') if user_input is None.
|
||||
Return await self.async_finish(flow_result) if login init step pass.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -13,9 +13,10 @@ from homeassistant.const import (
|
||||
CONF_PENDING_TIME, CONF_TRIGGER_TIME)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the Demo alarm control panel platform."""
|
||||
add_entities([
|
||||
async_add_entities([
|
||||
manual.ManualAlarm(hass, 'Alarm', '1234', None, False, {
|
||||
STATE_ALARM_ARMED_AWAY: {
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
|
||||
@@ -12,10 +12,10 @@ import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyialarm==0.2']
|
||||
REQUIREMENTS = ['pyialarm==0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -89,6 +89,8 @@ class IAlarmPanel(alarm.AlarmControlPanel):
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif status == self._client.ARMED_STAY:
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
elif status == self._client.TRIGGERED:
|
||||
state = STATE_ALARM_TRIGGERED
|
||||
else:
|
||||
state = None
|
||||
|
||||
|
||||
67
homeassistant/components/alarm_control_panel/lupusec.py
Normal file
67
homeassistant/components/alarm_control_panel/lupusec.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
This component provides HA alarm_control_panel support for Lupusec System.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.lupusec/
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.components.lupusec import DOMAIN as LUPUSEC_DOMAIN
|
||||
from homeassistant.components.lupusec import LupusecDevice
|
||||
from homeassistant.const import (STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED)
|
||||
|
||||
DEPENDENCIES = ['lupusec']
|
||||
|
||||
ICON = 'mdi:security'
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=2)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up an alarm control panel for a Lupusec device."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
data = hass.data[LUPUSEC_DOMAIN]
|
||||
|
||||
alarm_devices = [LupusecAlarm(data, data.lupusec.get_alarm())]
|
||||
|
||||
add_entities(alarm_devices)
|
||||
|
||||
|
||||
class LupusecAlarm(LupusecDevice, AlarmControlPanel):
|
||||
"""An alarm_control_panel implementation for Lupusec."""
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon."""
|
||||
return ICON
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._device.is_standby:
|
||||
state = STATE_ALARM_DISARMED
|
||||
elif self._device.is_away:
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif self._device.is_home:
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
state = None
|
||||
return state
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._device.set_away()
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._device.set_standby()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._device.set_home()
|
||||
@@ -21,6 +21,7 @@ from homeassistant.const import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -306,3 +307,10 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
state_attr[ATTR_POST_PENDING_STATE] = self._state
|
||||
|
||||
return state_attr
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added to hass."""
|
||||
state = await async_get_last_state(self.hass, self.entity_id)
|
||||
if state:
|
||||
self._state = state.state
|
||||
self._state_ts = state.last_updated
|
||||
|
||||
@@ -335,11 +335,8 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
return state_attr
|
||||
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe to MQTT events.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to MQTT events."""
|
||||
async_track_state_change(
|
||||
self.hass, self.entity_id, self._async_state_changed_listener
|
||||
)
|
||||
@@ -359,7 +356,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
_LOGGER.warning("Received unexpected payload: %s", payload)
|
||||
return
|
||||
|
||||
return mqtt.async_subscribe(
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._command_topic, message_received, self._qos)
|
||||
|
||||
async def _async_state_changed_listener(self, entity_id, old_state,
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.20']
|
||||
REQUIREMENTS = ['total_connect_client==0.22']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -24,23 +24,25 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'alert'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
CONF_DONE_MESSAGE = 'done_message'
|
||||
CONF_CAN_ACK = 'can_acknowledge'
|
||||
CONF_NOTIFIERS = 'notifiers'
|
||||
CONF_REPEAT = 'repeat'
|
||||
CONF_SKIP_FIRST = 'skip_first'
|
||||
CONF_ALERT_MESSAGE = 'message'
|
||||
CONF_DONE_MESSAGE = 'done_message'
|
||||
|
||||
DEFAULT_CAN_ACK = True
|
||||
DEFAULT_SKIP_FIRST = False
|
||||
|
||||
ALERT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_DONE_MESSAGE): cv.string,
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
|
||||
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
|
||||
vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
|
||||
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
|
||||
vol.Optional(CONF_ALERT_MESSAGE): cv.template,
|
||||
vol.Optional(CONF_DONE_MESSAGE): cv.template,
|
||||
vol.Required(CONF_NOTIFIERS): cv.ensure_list})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
@@ -62,31 +64,47 @@ def is_on(hass, entity_id):
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the Alert component."""
|
||||
alerts = config.get(DOMAIN)
|
||||
all_alerts = {}
|
||||
entities = []
|
||||
|
||||
for object_id, cfg in config[DOMAIN].items():
|
||||
if not cfg:
|
||||
cfg = {}
|
||||
|
||||
name = cfg.get(CONF_NAME)
|
||||
watched_entity_id = cfg.get(CONF_ENTITY_ID)
|
||||
alert_state = cfg.get(CONF_STATE)
|
||||
repeat = cfg.get(CONF_REPEAT)
|
||||
skip_first = cfg.get(CONF_SKIP_FIRST)
|
||||
message_template = cfg.get(CONF_ALERT_MESSAGE)
|
||||
done_message_template = cfg.get(CONF_DONE_MESSAGE)
|
||||
notifiers = cfg.get(CONF_NOTIFIERS)
|
||||
can_ack = cfg.get(CONF_CAN_ACK)
|
||||
|
||||
entities.append(Alert(hass, object_id, name,
|
||||
watched_entity_id, alert_state, repeat,
|
||||
skip_first, message_template,
|
||||
done_message_template, notifiers,
|
||||
can_ack))
|
||||
|
||||
if not entities:
|
||||
return False
|
||||
|
||||
async def async_handle_alert_service(service_call):
|
||||
"""Handle calls to alert services."""
|
||||
alert_ids = service.extract_entity_ids(hass, service_call)
|
||||
|
||||
for alert_id in alert_ids:
|
||||
alert = all_alerts[alert_id]
|
||||
alert.async_set_context(service_call.context)
|
||||
if service_call.service == SERVICE_TURN_ON:
|
||||
await alert.async_turn_on()
|
||||
elif service_call.service == SERVICE_TOGGLE:
|
||||
await alert.async_toggle()
|
||||
else:
|
||||
await alert.async_turn_off()
|
||||
for alert in entities:
|
||||
if alert.entity_id != alert_id:
|
||||
continue
|
||||
|
||||
# Setup alerts
|
||||
for entity_id, alert in alerts.items():
|
||||
entity = Alert(hass, entity_id,
|
||||
alert[CONF_NAME], alert.get(CONF_DONE_MESSAGE),
|
||||
alert[CONF_ENTITY_ID], alert[CONF_STATE],
|
||||
alert[CONF_REPEAT], alert[CONF_SKIP_FIRST],
|
||||
alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK])
|
||||
all_alerts[entity.entity_id] = entity
|
||||
alert.async_set_context(service_call.context)
|
||||
if service_call.service == SERVICE_TURN_ON:
|
||||
await alert.async_turn_on()
|
||||
elif service_call.service == SERVICE_TOGGLE:
|
||||
await alert.async_toggle()
|
||||
else:
|
||||
await alert.async_turn_off()
|
||||
|
||||
# Setup service calls
|
||||
hass.services.async_register(
|
||||
@@ -99,7 +117,7 @@ async def async_setup(hass, config):
|
||||
DOMAIN, SERVICE_TOGGLE, async_handle_alert_service,
|
||||
schema=ALERT_SERVICE_SCHEMA)
|
||||
|
||||
tasks = [alert.async_update_ha_state() for alert in all_alerts.values()]
|
||||
tasks = [alert.async_update_ha_state() for alert in entities]
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@@ -109,16 +127,25 @@ async def async_setup(hass, config):
|
||||
class Alert(ToggleEntity):
|
||||
"""Representation of an alert."""
|
||||
|
||||
def __init__(self, hass, entity_id, name, done_message, watched_entity_id,
|
||||
state, repeat, skip_first, notifiers, can_ack):
|
||||
def __init__(self, hass, entity_id, name, watched_entity_id,
|
||||
state, repeat, skip_first, message_template,
|
||||
done_message_template, notifiers, can_ack):
|
||||
"""Initialize the alert."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._alert_state = state
|
||||
self._skip_first = skip_first
|
||||
|
||||
self._message_template = message_template
|
||||
if self._message_template is not None:
|
||||
self._message_template.hass = hass
|
||||
|
||||
self._done_message_template = done_message_template
|
||||
if self._done_message_template is not None:
|
||||
self._done_message_template.hass = hass
|
||||
|
||||
self._notifiers = notifiers
|
||||
self._can_ack = can_ack
|
||||
self._done_message = done_message
|
||||
|
||||
self._delay = [timedelta(minutes=val) for val in repeat]
|
||||
self._next_delay = 0
|
||||
@@ -184,7 +211,7 @@ class Alert(ToggleEntity):
|
||||
self._cancel()
|
||||
self._ack = False
|
||||
self._firing = False
|
||||
if self._done_message and self._send_done_message:
|
||||
if self._send_done_message:
|
||||
await self._notify_done_message()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@@ -204,18 +231,31 @@ class Alert(ToggleEntity):
|
||||
if not self._ack:
|
||||
_LOGGER.info("Alerting: %s", self._name)
|
||||
self._send_done_message = True
|
||||
for target in self._notifiers:
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN_NOTIFY, target, {ATTR_MESSAGE: self._name})
|
||||
|
||||
if self._message_template is not None:
|
||||
message = self._message_template.async_render()
|
||||
else:
|
||||
message = self._name
|
||||
|
||||
await self._send_notification_message(message)
|
||||
await self._schedule_notify()
|
||||
|
||||
async def _notify_done_message(self, *args):
|
||||
"""Send notification of complete alert."""
|
||||
_LOGGER.info("Alerting: %s", self._done_message)
|
||||
_LOGGER.info("Alerting: %s", self._done_message_template)
|
||||
self._send_done_message = False
|
||||
|
||||
if self._done_message_template is None:
|
||||
return
|
||||
|
||||
message = self._done_message_template.async_render()
|
||||
|
||||
await self._send_notification_message(message)
|
||||
|
||||
async def _send_notification_message(self, message):
|
||||
for target in self._notifiers:
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN_NOTIFY, target, {ATTR_MESSAGE: self._done_message})
|
||||
DOMAIN_NOTIFY, target, {ATTR_MESSAGE: message})
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Async Unacknowledge alert."""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
68
homeassistant/components/asuswrt.py
Normal file
68
homeassistant/components/asuswrt.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
Support for ASUSWRT devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/asuswrt/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_MODE,
|
||||
CONF_PROTOCOL)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
|
||||
REQUIREMENTS = ['aioasuswrt==1.1.11']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "asuswrt"
|
||||
DATA_ASUSWRT = DOMAIN
|
||||
|
||||
CONF_PUB_KEY = 'pub_key'
|
||||
CONF_SSH_KEY = 'ssh_key'
|
||||
CONF_REQUIRE_IP = 'require_ip'
|
||||
DEFAULT_SSH_PORT = 22
|
||||
SECRET_GROUP = 'Password or SSH Key'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']),
|
||||
vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']),
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port,
|
||||
vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean,
|
||||
vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string,
|
||||
vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile,
|
||||
vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the asuswrt component."""
|
||||
from aioasuswrt.asuswrt import AsusWrt
|
||||
conf = config[DOMAIN]
|
||||
|
||||
api = AsusWrt(conf[CONF_HOST], conf.get(CONF_PORT),
|
||||
conf.get(CONF_PROTOCOL) == 'telnet',
|
||||
conf[CONF_USERNAME],
|
||||
conf.get(CONF_PASSWORD, ''),
|
||||
conf.get('ssh_key', conf.get('pub_key', '')),
|
||||
conf.get(CONF_MODE), conf.get(CONF_REQUIRE_IP))
|
||||
|
||||
await api.connection.async_connect()
|
||||
if not api.is_connected:
|
||||
_LOGGER.error("Unable to setup asuswrt component")
|
||||
return False
|
||||
|
||||
hass.data[DATA_ASUSWRT] = api
|
||||
|
||||
hass.async_create_task(async_load_platform(
|
||||
hass, 'sensor', DOMAIN, {}, config))
|
||||
hass.async_create_task(async_load_platform(
|
||||
hass, 'device_tracker', DOMAIN, {}, config))
|
||||
return True
|
||||
@@ -11,8 +11,9 @@ import voluptuous as vol
|
||||
from requests import RequestException
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT)
|
||||
CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
@@ -20,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_CONFIGURING = {}
|
||||
|
||||
REQUIREMENTS = ['py-august==0.6.0']
|
||||
REQUIREMENTS = ['py-august==0.7.0']
|
||||
|
||||
DEFAULT_TIMEOUT = 10
|
||||
ACTIVITY_FETCH_LIMIT = 10
|
||||
@@ -116,7 +117,8 @@ def setup_august(hass, config, api, authenticator):
|
||||
if DOMAIN in _CONFIGURING:
|
||||
hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN))
|
||||
|
||||
hass.data[DATA_AUGUST] = AugustData(api, authentication.access_token)
|
||||
hass.data[DATA_AUGUST] = AugustData(
|
||||
hass, api, authentication.access_token)
|
||||
|
||||
for component in AUGUST_COMPONENTS:
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
@@ -136,9 +138,16 @@ def setup(hass, config):
|
||||
"""Set up the August component."""
|
||||
from august.api import Api
|
||||
from august.authenticator import Authenticator
|
||||
from requests import Session
|
||||
|
||||
conf = config[DOMAIN]
|
||||
api = Api(timeout=conf.get(CONF_TIMEOUT))
|
||||
try:
|
||||
api_http_session = Session()
|
||||
except RequestException as ex:
|
||||
_LOGGER.warning("Creating HTTP session failed with: %s", str(ex))
|
||||
api_http_session = None
|
||||
|
||||
api = Api(timeout=conf.get(CONF_TIMEOUT), http_session=api_http_session)
|
||||
|
||||
authenticator = Authenticator(
|
||||
api,
|
||||
@@ -154,8 +163,9 @@ def setup(hass, config):
|
||||
class AugustData:
|
||||
"""August data object."""
|
||||
|
||||
def __init__(self, api, access_token):
|
||||
def __init__(self, hass, api, access_token):
|
||||
"""Init August data object."""
|
||||
self._hass = hass
|
||||
self._api = api
|
||||
self._access_token = access_token
|
||||
self._doorbells = self._api.get_doorbells(self._access_token) or []
|
||||
@@ -168,6 +178,22 @@ class AugustData:
|
||||
self._door_state_by_id = {}
|
||||
self._activities_by_id = {}
|
||||
|
||||
@callback
|
||||
def august_api_stop(event):
|
||||
"""Close the API HTTP session."""
|
||||
_LOGGER.debug("Closing August HTTP session")
|
||||
|
||||
try:
|
||||
self._api.http_session.close()
|
||||
self._api.http_session = None
|
||||
except RequestException:
|
||||
pass
|
||||
_LOGGER.debug("August HTTP session closed.")
|
||||
|
||||
self._hass.bus.listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, august_api_stop)
|
||||
_LOGGER.debug("Registered for HASS stop event")
|
||||
|
||||
@property
|
||||
def house_ids(self):
|
||||
"""Return a list of house_ids."""
|
||||
@@ -201,8 +227,11 @@ class AugustData:
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
||||
"""Update data object with latest from August API."""
|
||||
_LOGGER.debug("Updating device activities")
|
||||
_LOGGER.debug("Start retrieving device activities")
|
||||
for house_id in self.house_ids:
|
||||
_LOGGER.debug("Updating device activity for house id %s",
|
||||
house_id)
|
||||
|
||||
activities = self._api.get_house_activities(self._access_token,
|
||||
house_id,
|
||||
limit=limit)
|
||||
@@ -211,6 +240,7 @@ class AugustData:
|
||||
for device_id in device_ids:
|
||||
self._activities_by_id[device_id] = [a for a in activities if
|
||||
a.device_id == device_id]
|
||||
_LOGGER.debug("Completed retrieving device activities")
|
||||
|
||||
def get_doorbell_detail(self, doorbell_id):
|
||||
"""Return doorbell detail."""
|
||||
@@ -223,10 +253,19 @@ class AugustData:
|
||||
|
||||
_LOGGER.debug("Start retrieving doorbell details")
|
||||
for doorbell in self._doorbells:
|
||||
_LOGGER.debug("Updating status for %s",
|
||||
_LOGGER.debug("Updating doorbell status for %s",
|
||||
doorbell.device_name)
|
||||
detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail(
|
||||
self._access_token, doorbell.device_id)
|
||||
try:
|
||||
detail_by_id[doorbell.device_id] =\
|
||||
self._api.get_doorbell_detail(
|
||||
self._access_token, doorbell.device_id)
|
||||
except RequestException as ex:
|
||||
_LOGGER.error("Request error trying to retrieve doorbell"
|
||||
" status for %s. %s", doorbell.device_name, ex)
|
||||
detail_by_id[doorbell.device_id] = None
|
||||
except Exception:
|
||||
detail_by_id[doorbell.device_id] = None
|
||||
raise
|
||||
|
||||
_LOGGER.debug("Completed retrieving doorbell details")
|
||||
self._doorbell_detail_by_id = detail_by_id
|
||||
@@ -258,10 +297,19 @@ class AugustData:
|
||||
|
||||
_LOGGER.debug("Start retrieving door status")
|
||||
for lock in self._locks:
|
||||
_LOGGER.debug("Updating status for %s",
|
||||
_LOGGER.debug("Updating door status for %s",
|
||||
lock.device_name)
|
||||
state_by_id[lock.device_id] = self._api.get_lock_door_status(
|
||||
self._access_token, lock.device_id)
|
||||
|
||||
try:
|
||||
state_by_id[lock.device_id] = self._api.get_lock_door_status(
|
||||
self._access_token, lock.device_id)
|
||||
except RequestException as ex:
|
||||
_LOGGER.error("Request error trying to retrieve door"
|
||||
" status for %s. %s", lock.device_name, ex)
|
||||
state_by_id[lock.device_id] = None
|
||||
except Exception:
|
||||
state_by_id[lock.device_id] = None
|
||||
raise
|
||||
|
||||
_LOGGER.debug("Completed retrieving door status")
|
||||
self._door_state_by_id = state_by_id
|
||||
@@ -273,12 +321,29 @@ class AugustData:
|
||||
|
||||
_LOGGER.debug("Start retrieving locks status")
|
||||
for lock in self._locks:
|
||||
_LOGGER.debug("Updating status for %s",
|
||||
_LOGGER.debug("Updating lock status for %s",
|
||||
lock.device_name)
|
||||
status_by_id[lock.device_id] = self._api.get_lock_status(
|
||||
self._access_token, lock.device_id)
|
||||
detail_by_id[lock.device_id] = self._api.get_lock_detail(
|
||||
self._access_token, lock.device_id)
|
||||
try:
|
||||
status_by_id[lock.device_id] = self._api.get_lock_status(
|
||||
self._access_token, lock.device_id)
|
||||
except RequestException as ex:
|
||||
_LOGGER.error("Request error trying to retrieve door"
|
||||
" status for %s. %s", lock.device_name, ex)
|
||||
status_by_id[lock.device_id] = None
|
||||
except Exception:
|
||||
status_by_id[lock.device_id] = None
|
||||
raise
|
||||
|
||||
try:
|
||||
detail_by_id[lock.device_id] = self._api.get_lock_detail(
|
||||
self._access_token, lock.device_id)
|
||||
except RequestException as ex:
|
||||
_LOGGER.error("Request error trying to retrieve door"
|
||||
" details for %s. %s", lock.device_name, ex)
|
||||
detail_by_id[lock.device_id] = None
|
||||
except Exception:
|
||||
detail_by_id[lock.device_id] = None
|
||||
raise
|
||||
|
||||
_LOGGER.debug("Completed retrieving locks status")
|
||||
self._lock_status_by_id = status_by_id
|
||||
|
||||
26
homeassistant/components/auth/.translations/cs.json
Normal file
26
homeassistant/components/auth/.translations/cs.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "\u017d\u00e1dn\u00e9 oznamovac\u00ed slu\u017eby nejsou k dispozici."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "Neplatn\u00fd k\u00f3d, zkuste to znovu."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Vyberte pros\u00edm jednu z oznamovac\u00edch slu\u017eeb:",
|
||||
"title": "Nastavte jednor\u00e1zov\u00e9 heslo dodan\u00e9 komponentou notify"
|
||||
},
|
||||
"setup": {
|
||||
"title": "Ov\u011b\u0159en\u00ed nastaven\u00ed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Neplatn\u00fd k\u00f3d, zkuste to znovu. Pokud se tato chyba opakuje, ujist\u011bte se, \u017ee hodiny syst\u00e9mu Home Assistant jsou spr\u00e1vn\u011b nastaveny."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
homeassistant/components/auth/.translations/es.json
Normal file
35
homeassistant/components/auth/.translations/es.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "No hay servicios de notificaci\u00f3n disponibles."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "C\u00f3digo inv\u00e1lido, por favor int\u00e9ntelo de nuevo."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Seleccione uno de los servicios de notificaci\u00f3n:",
|
||||
"title": "Configure una contrase\u00f1a de un solo uso entregada por el componente de notificaci\u00f3n"
|
||||
},
|
||||
"setup": {
|
||||
"description": "Se ha enviado una contrase\u00f1a de un solo uso a trav\u00e9s de ** notificar. {notify_service} **. Por favor introd\u00facela a continuaci\u00f3n:",
|
||||
"title": "Verificar la configuraci\u00f3n"
|
||||
}
|
||||
},
|
||||
"title": "Notificar la contrase\u00f1a de un solo uso"
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"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} ` **.",
|
||||
"title": "Configure la autenticaci\u00f3n de dos factores utilizando TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,27 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "Nessun servizio di notifica disponibile."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "Codice non valido, per favore riprovare."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Selezionare uno dei servizi di notifica:"
|
||||
},
|
||||
"setup": {
|
||||
"description": "\u00c8 stata inviata una password monouso tramite **notify.{notify_service}**. Per favore, inseriscila qui sotto:",
|
||||
"title": "Verifica l'installazione"
|
||||
}
|
||||
},
|
||||
"title": "Notifica la Password monouso"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Codice non valido, per favore riprovare. Se riscontri spesso questo errore, assicurati che l'orologio del sistema Home Assistant sia accurato."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Per attivare l'autenticazione a due fattori utilizzando password monouso basate sul tempo, eseguire la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \n Dopo aver scansionato il codice, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con codice ** ` {code} ` **.",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"step": {
|
||||
"setup": {
|
||||
"title": "Verificar a configura\u00e7\u00e3o"
|
||||
}
|
||||
}
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "C\u00f3digo inv\u00e1lido, por favor tente novamente. Se voc\u00ea obtiver este erro de forma consistente, certifique-se de que o rel\u00f3gio do sistema Home Assistant esteja correto."
|
||||
|
||||
@@ -10,22 +10,22 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Por favor, selecione um dos servi\u00e7os de notifica\u00e7\u00e3o:",
|
||||
"title": "Configurar uma palavra passe entregue pela componente de notifica\u00e7\u00e3o"
|
||||
"title": "Configurar uma palavra-passe entregue pela componente de notifica\u00e7\u00e3o"
|
||||
},
|
||||
"setup": {
|
||||
"description": "Foi enviada uma palavra passe atrav\u00e9s de **notify.{notify_service}**. Por favor, insira-a:",
|
||||
"description": "Foi enviada uma palavra-passe atrav\u00e9s de **notify.{notify_service}**. Por favor, insira-a:",
|
||||
"title": "Verificar a configura\u00e7\u00e3o"
|
||||
}
|
||||
},
|
||||
"title": "Notificar palavra passe de uso \u00fanico"
|
||||
"title": "Notificar palavra-passe de uso \u00fanico"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "C\u00f3digo inv\u00e1lido, por favor, tente novamente. Se receber este erro constantemente, por favor, certifique-se de que o rel\u00f3gio do sistema que hospeda o Home Assistent \u00e9 preciso."
|
||||
"invalid_code": "C\u00f3digo inv\u00e1lido, por favor, tente novamente. Se receber este erro constantemente, por favor, certifique-se de que o rel\u00f3gio do sistema que hospeda o Home Assistant \u00e9 preciso."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Para ativar a autentica\u00e7\u00e3o com dois fatores utilizando passwords unicas temporais (OTP), ler o c\u00f3digo QR com a sua aplica\u00e7\u00e3o de autentica\u00e7\u00e3o. Se voc\u00ea n\u00e3o tiver uma, recomendamos [Google Authenticator](https://support.google.com/accounts/answer/1066447) ou [Authy](https://authy.com/).\n\n{qr_code}\n\nDepois de ler o c\u00f3digo, introduza o c\u00f3digo de seis d\u00edgitos fornecido pela sua aplica\u00e7\u00e3o para verificar a configura\u00e7\u00e3o. Se tiver problemas a ler o c\u00f3digo QR, fa\u00e7a uma configura\u00e7\u00e3o manual com o c\u00f3digo **`{c\u00f3digo}`**.",
|
||||
"description": "Para ativar a autentica\u00e7\u00e3o com dois fatores utilizando palavras-passe de uso \u00fanico (OTP), ler o c\u00f3digo QR com a sua aplica\u00e7\u00e3o de autentica\u00e7\u00e3o. Se n\u00e3o tiver uma, recomendamos [Google Authenticator](https://support.google.com/accounts/answer/1066447) ou [Authy](https://authy.com/).\n\n{qr_code}\n\nDepois de ler o c\u00f3digo, introduza o c\u00f3digo de seis d\u00edgitos fornecido pela sua aplica\u00e7\u00e3o para verificar a configura\u00e7\u00e3o. Se tiver problemas a ler o c\u00f3digo QR, fa\u00e7a uma configura\u00e7\u00e3o manual com o c\u00f3digo **`{code}`**.",
|
||||
"title": "Configurar autentica\u00e7\u00e3o com dois fatores usando TOTP"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -129,6 +129,7 @@ from homeassistant.auth.models import User, Credentials, \
|
||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.http import KEY_REAL_IP
|
||||
from homeassistant.components.http.auth import async_sign_path
|
||||
from homeassistant.components.http.ban import log_invalid_auth
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
@@ -169,6 +170,14 @@ SCHEMA_WS_DELETE_REFRESH_TOKEN = \
|
||||
vol.Required('refresh_token_id'): str,
|
||||
})
|
||||
|
||||
WS_TYPE_SIGN_PATH = 'auth/sign_path'
|
||||
SCHEMA_WS_SIGN_PATH = \
|
||||
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_SIGN_PATH,
|
||||
vol.Required('path'): str,
|
||||
vol.Optional('expires', default=30): int,
|
||||
})
|
||||
|
||||
RESULT_TYPE_CREDENTIALS = 'credentials'
|
||||
RESULT_TYPE_USER = 'user'
|
||||
|
||||
@@ -201,6 +210,11 @@ async def async_setup(hass, config):
|
||||
websocket_delete_refresh_token,
|
||||
SCHEMA_WS_DELETE_REFRESH_TOKEN
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_SIGN_PATH,
|
||||
websocket_sign_path,
|
||||
SCHEMA_WS_SIGN_PATH
|
||||
)
|
||||
|
||||
await login_flow.async_setup(hass, store_result)
|
||||
await mfa_setup_flow.async_setup(hass)
|
||||
@@ -424,54 +438,46 @@ def _create_auth_code_store():
|
||||
|
||||
|
||||
@websocket_api.ws_require_user()
|
||||
@callback
|
||||
def websocket_current_user(
|
||||
@websocket_api.async_response
|
||||
async def websocket_current_user(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
"""Return the current user."""
|
||||
async def async_get_current_user(user):
|
||||
"""Get current user."""
|
||||
enabled_modules = await hass.auth.async_get_enabled_mfa(user)
|
||||
user = connection.user
|
||||
enabled_modules = await hass.auth.async_get_enabled_mfa(user)
|
||||
|
||||
connection.send_message(
|
||||
websocket_api.result_message(msg['id'], {
|
||||
'id': user.id,
|
||||
'name': user.name,
|
||||
'is_owner': user.is_owner,
|
||||
'credentials': [{'auth_provider_type': c.auth_provider_type,
|
||||
'auth_provider_id': c.auth_provider_id}
|
||||
for c in user.credentials],
|
||||
'mfa_modules': [{
|
||||
'id': module.id,
|
||||
'name': module.name,
|
||||
'enabled': module.id in enabled_modules,
|
||||
} for module in hass.auth.auth_mfa_modules],
|
||||
}))
|
||||
|
||||
hass.async_create_task(async_get_current_user(connection.user))
|
||||
connection.send_message(
|
||||
websocket_api.result_message(msg['id'], {
|
||||
'id': user.id,
|
||||
'name': user.name,
|
||||
'is_owner': user.is_owner,
|
||||
'credentials': [{'auth_provider_type': c.auth_provider_type,
|
||||
'auth_provider_id': c.auth_provider_id}
|
||||
for c in user.credentials],
|
||||
'mfa_modules': [{
|
||||
'id': module.id,
|
||||
'name': module.name,
|
||||
'enabled': module.id in enabled_modules,
|
||||
} for module in hass.auth.auth_mfa_modules],
|
||||
}))
|
||||
|
||||
|
||||
@websocket_api.ws_require_user()
|
||||
@callback
|
||||
def websocket_create_long_lived_access_token(
|
||||
@websocket_api.async_response
|
||||
async def websocket_create_long_lived_access_token(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
"""Create or a long-lived access token."""
|
||||
async def async_create_long_lived_access_token(user):
|
||||
"""Create or a long-lived access token."""
|
||||
refresh_token = await hass.auth.async_create_refresh_token(
|
||||
user,
|
||||
client_name=msg['client_name'],
|
||||
client_icon=msg.get('client_icon'),
|
||||
token_type=TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
||||
access_token_expiration=timedelta(days=msg['lifespan']))
|
||||
refresh_token = await hass.auth.async_create_refresh_token(
|
||||
connection.user,
|
||||
client_name=msg['client_name'],
|
||||
client_icon=msg.get('client_icon'),
|
||||
token_type=TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
||||
access_token_expiration=timedelta(days=msg['lifespan']))
|
||||
|
||||
access_token = hass.auth.async_create_access_token(
|
||||
refresh_token)
|
||||
access_token = hass.auth.async_create_access_token(
|
||||
refresh_token)
|
||||
|
||||
connection.send_message(
|
||||
websocket_api.result_message(msg['id'], access_token))
|
||||
|
||||
hass.async_create_task(
|
||||
async_create_long_lived_access_token(connection.user))
|
||||
connection.send_message(
|
||||
websocket_api.result_message(msg['id'], access_token))
|
||||
|
||||
|
||||
@websocket_api.ws_require_user()
|
||||
@@ -494,22 +500,28 @@ def websocket_refresh_tokens(
|
||||
|
||||
|
||||
@websocket_api.ws_require_user()
|
||||
@callback
|
||||
def websocket_delete_refresh_token(
|
||||
@websocket_api.async_response
|
||||
async def websocket_delete_refresh_token(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
"""Handle a delete refresh token request."""
|
||||
async def async_delete_refresh_token(user, refresh_token_id):
|
||||
"""Delete a refresh token."""
|
||||
refresh_token = connection.user.refresh_tokens.get(refresh_token_id)
|
||||
refresh_token = connection.user.refresh_tokens.get(msg['refresh_token_id'])
|
||||
|
||||
if refresh_token is None:
|
||||
return websocket_api.error_message(
|
||||
msg['id'], 'invalid_token_id', 'Received invalid token')
|
||||
if refresh_token is None:
|
||||
return websocket_api.error_message(
|
||||
msg['id'], 'invalid_token_id', 'Received invalid token')
|
||||
|
||||
await hass.auth.async_remove_refresh_token(refresh_token)
|
||||
await hass.auth.async_remove_refresh_token(refresh_token)
|
||||
|
||||
connection.send_message(
|
||||
websocket_api.result_message(msg['id'], {}))
|
||||
connection.send_message(
|
||||
websocket_api.result_message(msg['id'], {}))
|
||||
|
||||
hass.async_create_task(
|
||||
async_delete_refresh_token(connection.user, msg['refresh_token_id']))
|
||||
|
||||
@websocket_api.ws_require_user()
|
||||
@callback
|
||||
def websocket_sign_path(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
"""Handle a sign path request."""
|
||||
connection.send_message(websocket_api.result_message(msg['id'], {
|
||||
'path': async_sign_path(hass, connection.refresh_token_id, msg['path'],
|
||||
timedelta(seconds=msg['expires']))
|
||||
}))
|
||||
|
||||
@@ -400,6 +400,9 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
removes = []
|
||||
info = {
|
||||
'name': name
|
||||
}
|
||||
|
||||
for conf in trigger_configs:
|
||||
platform = await async_prepare_setup_platform(
|
||||
@@ -408,7 +411,7 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action):
|
||||
if platform is None:
|
||||
return None
|
||||
|
||||
remove = await platform.async_trigger(hass, conf, action)
|
||||
remove = await platform.async_trigger(hass, conf, action, info)
|
||||
|
||||
if not remove:
|
||||
_LOGGER.error("Error setting up trigger %s", name)
|
||||
|
||||
@@ -24,7 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action, automation_info):
|
||||
"""Listen for events based on configuration."""
|
||||
event_type = config.get(CONF_EVENT_TYPE)
|
||||
event_data_schema = vol.Schema(
|
||||
|
||||
@@ -33,7 +33,7 @@ def source_match(state, source):
|
||||
return state and state.attributes.get('source') == source
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action, automation_info):
|
||||
"""Listen for state changes based on configuration."""
|
||||
source = config.get(CONF_SOURCE).lower()
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
|
||||
@@ -22,7 +22,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action, automation_info):
|
||||
"""Listen for events based on configuration."""
|
||||
event = config.get(CONF_EVENT)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action, automation_info):
|
||||
"""Listen for events based on configuration."""
|
||||
number = config.get(CONF_NUMBER)
|
||||
held_more_than = config.get(CONF_HELD_MORE_THAN)
|
||||
|
||||
@@ -24,7 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action, automation_info):
|
||||
"""Listen for state changes based on configuration."""
|
||||
topic = config.get(CONF_TOPIC)
|
||||
payload = config.get(CONF_PAYLOAD)
|
||||
|
||||
@@ -28,7 +28,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action, automation_info):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
below = config.get(CONF_BELOW)
|
||||
|
||||
@@ -26,7 +26,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
}), cv.key_dependency(CONF_FOR, CONF_TO))
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action, automation_info):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||
|
||||
@@ -24,7 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action, automation_info):
|
||||
"""Listen for events based on configuration."""
|
||||
event = config.get(CONF_EVENT)
|
||||
offset = config.get(CONF_OFFSET)
|
||||
|
||||
@@ -22,7 +22,7 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action, automation_info):
|
||||
"""Listen for state changes based on configuration."""
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
value_template.hass = hass
|
||||
|
||||
@@ -28,7 +28,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT))
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action, automation_info):
|
||||
"""Listen for state changes based on configuration."""
|
||||
if CONF_AT in config:
|
||||
at_time = config.get(CONF_AT)
|
||||
|
||||
@@ -14,6 +14,8 @@ from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_PLATFORM, CONF_WEBHOOK_ID
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import DOMAIN as AUTOMATION_DOMAIN
|
||||
|
||||
DEPENDENCIES = ('webhook',)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -39,10 +41,11 @@ async def _handle_webhook(action, hass, webhook_id, request):
|
||||
hass.async_run_job(action, {'trigger': result})
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action, automation_info):
|
||||
"""Trigger based on incoming webhooks."""
|
||||
webhook_id = config.get(CONF_WEBHOOK_ID)
|
||||
hass.components.webhook.async_register(
|
||||
AUTOMATION_DOMAIN, automation_info['name'],
|
||||
webhook_id, partial(_handle_webhook, action))
|
||||
|
||||
@callback
|
||||
|
||||
@@ -26,7 +26,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action, automation_info):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
|
||||
@@ -10,24 +10,21 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.discovery import SERVICE_AXIS
|
||||
from homeassistant.const import (
|
||||
ATTR_LOCATION, ATTR_TRIPPED, CONF_EVENT, CONF_HOST, CONF_INCLUDE,
|
||||
ATTR_LOCATION, CONF_EVENT, CONF_HOST, CONF_INCLUDE,
|
||||
CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_TRIGGER_TIME, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
|
||||
REQUIREMENTS = ['axis==14']
|
||||
REQUIREMENTS = ['axis==16']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'axis'
|
||||
CONFIG_FILE = 'axis.conf'
|
||||
|
||||
AXIS_DEVICES = {}
|
||||
|
||||
EVENT_TYPES = ['motion', 'vmd3', 'pir', 'sound',
|
||||
'daynight', 'tampering', 'input']
|
||||
|
||||
@@ -99,8 +96,6 @@ def request_configuration(hass, config, name, host, serialnumber):
|
||||
return False
|
||||
|
||||
if setup_device(hass, config, device_config):
|
||||
del device_config['events']
|
||||
del device_config['signal']
|
||||
config_file = load_json(hass.config.path(CONFIG_FILE))
|
||||
config_file[serialnumber] = dict(device_config)
|
||||
save_json(hass.config.path(CONFIG_FILE), config_file)
|
||||
@@ -146,9 +141,11 @@ def request_configuration(hass, config, name, host, serialnumber):
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up for Axis devices."""
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
def _shutdown(call):
|
||||
"""Stop the event stream on shutdown."""
|
||||
for serialnumber, device in AXIS_DEVICES.items():
|
||||
for serialnumber, device in hass.data[DOMAIN].items():
|
||||
_LOGGER.info("Stopping event stream for %s.", serialnumber)
|
||||
device.stop()
|
||||
|
||||
@@ -160,7 +157,7 @@ def setup(hass, config):
|
||||
name = discovery_info['hostname']
|
||||
serialnumber = discovery_info['properties']['macaddress']
|
||||
|
||||
if serialnumber not in AXIS_DEVICES:
|
||||
if serialnumber not in hass.data[DOMAIN]:
|
||||
config_file = load_json(hass.config.path(CONFIG_FILE))
|
||||
if serialnumber in config_file:
|
||||
# Device config previously saved to file
|
||||
@@ -178,7 +175,7 @@ def setup(hass, config):
|
||||
request_configuration(hass, config, name, host, serialnumber)
|
||||
else:
|
||||
# Device already registered, but on a different IP
|
||||
device = AXIS_DEVICES[serialnumber]
|
||||
device = hass.data[DOMAIN][serialnumber]
|
||||
device.config.host = host
|
||||
dispatcher_send(hass, DOMAIN + '_' + device.name + '_new_ip', host)
|
||||
|
||||
@@ -195,7 +192,7 @@ def setup(hass, config):
|
||||
|
||||
def vapix_service(call):
|
||||
"""Service to send a message."""
|
||||
for _, device in AXIS_DEVICES.items():
|
||||
for device in hass.data[DOMAIN].values():
|
||||
if device.name == call.data[CONF_NAME]:
|
||||
response = device.vapix.do_request(
|
||||
call.data[SERVICE_CGI],
|
||||
@@ -214,7 +211,7 @@ def setup(hass, config):
|
||||
|
||||
def setup_device(hass, config, device_config):
|
||||
"""Set up an Axis device."""
|
||||
from axis import AxisDevice
|
||||
import axis
|
||||
|
||||
def signal_callback(action, event):
|
||||
"""Call to configure events when initialized on event stream."""
|
||||
@@ -229,18 +226,32 @@ def setup_device(hass, config, device_config):
|
||||
discovery.load_platform(
|
||||
hass, component, DOMAIN, event_config, config)
|
||||
|
||||
event_types = list(filter(lambda x: x in device_config[CONF_INCLUDE],
|
||||
EVENT_TYPES))
|
||||
device_config['events'] = event_types
|
||||
device_config['signal'] = signal_callback
|
||||
device = AxisDevice(hass.loop, **device_config)
|
||||
device.name = device_config[CONF_NAME]
|
||||
event_types = [
|
||||
event
|
||||
for event in device_config[CONF_INCLUDE]
|
||||
if event in EVENT_TYPES
|
||||
]
|
||||
|
||||
if device.serial_number is None:
|
||||
# If there is no serial number a connection could not be made
|
||||
_LOGGER.error("Couldn't connect to %s", device_config[CONF_HOST])
|
||||
device = axis.AxisDevice(
|
||||
loop=hass.loop, host=device_config[CONF_HOST],
|
||||
username=device_config[CONF_USERNAME],
|
||||
password=device_config[CONF_PASSWORD],
|
||||
port=device_config[CONF_PORT], web_proto='http',
|
||||
event_types=event_types, signal=signal_callback)
|
||||
|
||||
try:
|
||||
hass.data[DOMAIN][device.vapix.serial_number] = device
|
||||
|
||||
except axis.Unauthorized:
|
||||
_LOGGER.error("Credentials for %s are faulty",
|
||||
device_config[CONF_HOST])
|
||||
return False
|
||||
|
||||
except axis.RequestError:
|
||||
return False
|
||||
|
||||
device.name = device_config[CONF_NAME]
|
||||
|
||||
for component in device_config[CONF_INCLUDE]:
|
||||
if component == 'camera':
|
||||
camera_config = {
|
||||
@@ -253,51 +264,6 @@ def setup_device(hass, config, device_config):
|
||||
discovery.load_platform(
|
||||
hass, component, DOMAIN, camera_config, config)
|
||||
|
||||
AXIS_DEVICES[device.serial_number] = device
|
||||
if event_types:
|
||||
hass.add_job(device.start)
|
||||
return True
|
||||
|
||||
|
||||
class AxisDeviceEvent(Entity):
|
||||
"""Representation of a Axis device event."""
|
||||
|
||||
def __init__(self, event_config):
|
||||
"""Initialize the event."""
|
||||
self.axis_event = event_config[CONF_EVENT]
|
||||
self._name = '{}_{}_{}'.format(
|
||||
event_config[CONF_NAME], self.axis_event.event_type,
|
||||
self.axis_event.id)
|
||||
self.location = event_config[ATTR_LOCATION]
|
||||
self.axis_event.callback = self._update_callback
|
||||
|
||||
def _update_callback(self):
|
||||
"""Update the sensor's state, if needed."""
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the event."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the event."""
|
||||
return self.axis_event.event_class
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state. No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the event."""
|
||||
attr = {}
|
||||
|
||||
tripped = self.axis_event.is_tripped
|
||||
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
|
||||
|
||||
attr[ATTR_LOCATION] = self.location
|
||||
|
||||
return attr
|
||||
15
homeassistant/components/axis/services.yaml
Normal file
15
homeassistant/components/axis/services.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
vapix_call:
|
||||
description: Configure device using Vapix parameter management.
|
||||
fields:
|
||||
name:
|
||||
description: Name of device to Configure. [Required]
|
||||
example: M1065-W
|
||||
cgi:
|
||||
description: Which cgi to call on device. [Optional] Default is 'param.cgi'
|
||||
example: 'applications/control.cgi'
|
||||
action:
|
||||
description: What type of call. [Optional] Default is 'update'
|
||||
example: 'start'
|
||||
param:
|
||||
description: What parameter to operate on. [Required]
|
||||
example: 'package=VideoMotionDetection'
|
||||
@@ -19,14 +19,15 @@ SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
def _retrieve_door_state(data, lock):
|
||||
"""Get the latest state of the DoorSense sensor."""
|
||||
from august.lock import LockDoorStatus
|
||||
doorstate = data.get_door_state(lock.device_id)
|
||||
return doorstate == LockDoorStatus.OPEN
|
||||
return data.get_door_state(lock.device_id)
|
||||
|
||||
|
||||
def _retrieve_online_state(data, doorbell):
|
||||
"""Get the latest state of the sensor."""
|
||||
detail = data.get_doorbell_detail(doorbell.device_id)
|
||||
if detail is None:
|
||||
return None
|
||||
|
||||
return detail.is_online
|
||||
|
||||
|
||||
@@ -138,9 +139,10 @@ class AugustDoorBinarySensor(BinarySensorDevice):
|
||||
"""Get the latest state of the sensor."""
|
||||
state_provider = SENSOR_TYPES_DOOR[self._sensor_type][2]
|
||||
self._state = state_provider(self._data, self._door)
|
||||
self._available = self._state is not None
|
||||
|
||||
from august.lock import LockDoorStatus
|
||||
self._available = self._state != LockDoorStatus.UNKNOWN
|
||||
self._state = self._state == LockDoorStatus.OPEN
|
||||
|
||||
|
||||
class AugustDoorbellBinarySensor(BinarySensorDevice):
|
||||
@@ -152,6 +154,12 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
|
||||
self._sensor_type = sensor_type
|
||||
self._doorbell = doorbell
|
||||
self._state = None
|
||||
self._available = False
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return the availability of this sensor."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@@ -173,3 +181,4 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
|
||||
"""Get the latest state of the sensor."""
|
||||
state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][2]
|
||||
self._state = state_provider(self._data, self._doorbell)
|
||||
self._available = self._state is not None
|
||||
|
||||
@@ -7,10 +7,11 @@ https://home-assistant.io/components/binary_sensor.axis/
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.components.axis import AxisDeviceEvent
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import CONF_TRIGGER_TIME
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.const import (
|
||||
ATTR_LOCATION, CONF_EVENT, CONF_NAME, CONF_TRIGGER_TIME)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
DEPENDENCIES = ['axis']
|
||||
@@ -20,48 +21,71 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Axis binary devices."""
|
||||
add_entities([AxisBinarySensor(hass, discovery_info)], True)
|
||||
add_entities([AxisBinarySensor(discovery_info)], True)
|
||||
|
||||
|
||||
class AxisBinarySensor(AxisDeviceEvent, BinarySensorDevice):
|
||||
class AxisBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a binary Axis event."""
|
||||
|
||||
def __init__(self, hass, event_config):
|
||||
def __init__(self, event_config):
|
||||
"""Initialize the Axis binary sensor."""
|
||||
self.hass = hass
|
||||
self._state = False
|
||||
self._delay = event_config[CONF_TRIGGER_TIME]
|
||||
self._timer = None
|
||||
AxisDeviceEvent.__init__(self, event_config)
|
||||
self.axis_event = event_config[CONF_EVENT]
|
||||
self.device_name = event_config[CONF_NAME]
|
||||
self.location = event_config[ATTR_LOCATION]
|
||||
self.delay = event_config[CONF_TRIGGER_TIME]
|
||||
self.remove_timer = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe sensors events."""
|
||||
self.axis_event.callback = self._update_callback
|
||||
|
||||
def _update_callback(self):
|
||||
"""Update the sensor's state, if needed."""
|
||||
if self.remove_timer is not None:
|
||||
self.remove_timer()
|
||||
self.remove_timer = None
|
||||
|
||||
if self.delay == 0 or self.is_on:
|
||||
self.schedule_update_ha_state()
|
||||
else: # Run timer to delay updating the state
|
||||
@callback
|
||||
def _delay_update(now):
|
||||
"""Timer callback for sensor update."""
|
||||
_LOGGER.debug("%s called delayed (%s sec) update",
|
||||
self.name, self.delay)
|
||||
self.async_schedule_update_ha_state()
|
||||
self.remove_timer = None
|
||||
|
||||
self.remove_timer = async_track_point_in_utc_time(
|
||||
self.hass, _delay_update,
|
||||
utcnow() + timedelta(seconds=self.delay))
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if event is active."""
|
||||
return self._state
|
||||
return self.axis_event.is_tripped
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and update the state."""
|
||||
self._state = self.axis_event.is_tripped
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the event."""
|
||||
return '{}_{}_{}'.format(
|
||||
self.device_name, self.axis_event.event_type, self.axis_event.id)
|
||||
|
||||
def _update_callback(self):
|
||||
"""Update the sensor's state, if needed."""
|
||||
self.update()
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the event."""
|
||||
return self.axis_event.event_class
|
||||
|
||||
if self._timer is not None:
|
||||
self._timer()
|
||||
self._timer = None
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
if self._delay > 0 and not self.is_on:
|
||||
# Set timer to wait until updating the state
|
||||
def _delay_update(now):
|
||||
"""Timer callback for sensor update."""
|
||||
_LOGGER.debug("%s called delayed (%s sec) update",
|
||||
self._name, self._delay)
|
||||
self.schedule_update_ha_state()
|
||||
self._timer = None
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the event."""
|
||||
attr = {}
|
||||
|
||||
self._timer = track_point_in_utc_time(
|
||||
self.hass, _delay_update,
|
||||
utcnow() + timedelta(seconds=self._delay))
|
||||
else:
|
||||
self.schedule_update_ha_state()
|
||||
attr[ATTR_LOCATION] = self.location
|
||||
|
||||
return attr
|
||||
|
||||
@@ -6,8 +6,8 @@ https://home-assistant.io/components/binary_sensor.deconz/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.deconz.const import (
|
||||
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ,
|
||||
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN)
|
||||
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DECONZ_REACHABLE,
|
||||
DOMAIN as DECONZ_DOMAIN)
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
|
||||
@@ -24,6 +24,8 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the deCONZ binary sensor."""
|
||||
gateway = hass.data[DECONZ_DOMAIN]
|
||||
|
||||
@callback
|
||||
def async_add_sensor(sensors):
|
||||
"""Add binary sensor from deCONZ."""
|
||||
@@ -33,29 +35,35 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
for sensor in sensors:
|
||||
if sensor.type in DECONZ_BINARY_SENSOR and \
|
||||
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
|
||||
entities.append(DeconzBinarySensor(sensor))
|
||||
entities.append(DeconzBinarySensor(sensor, gateway))
|
||||
async_add_entities(entities, True)
|
||||
|
||||
hass.data[DATA_DECONZ_UNSUB].append(
|
||||
gateway.listeners.append(
|
||||
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
|
||||
|
||||
async_add_sensor(hass.data[DATA_DECONZ].sensors.values())
|
||||
async_add_sensor(gateway.api.sensors.values())
|
||||
|
||||
|
||||
class DeconzBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a binary sensor."""
|
||||
|
||||
def __init__(self, sensor):
|
||||
def __init__(self, sensor, gateway):
|
||||
"""Set up sensor and add update callback to get data from websocket."""
|
||||
self._sensor = sensor
|
||||
self.gateway = gateway
|
||||
self.unsub_dispatcher = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe sensors events."""
|
||||
self._sensor.register_async_callback(self.async_update_callback)
|
||||
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id
|
||||
self.gateway.deconz_ids[self.entity_id] = self._sensor.deconz_id
|
||||
self.unsub_dispatcher = async_dispatcher_connect(
|
||||
self.hass, DECONZ_REACHABLE, self.async_update_callback)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect sensor object when removed."""
|
||||
if self.unsub_dispatcher is not None:
|
||||
self.unsub_dispatcher()
|
||||
self._sensor.remove_callback(self.async_update_callback)
|
||||
self._sensor = None
|
||||
|
||||
@@ -100,7 +108,7 @@ class DeconzBinarySensor(BinarySensorDevice):
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if sensor is available."""
|
||||
return self._sensor.reachable
|
||||
return self.gateway.available and self._sensor.reachable
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -127,7 +135,7 @@ class DeconzBinarySensor(BinarySensorDevice):
|
||||
self._sensor.uniqueid.count(':') != 7):
|
||||
return None
|
||||
serial = self._sensor.uniqueid.split('-', 1)[0]
|
||||
bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid
|
||||
bridgeid = self.gateway.api.config.bridgeid
|
||||
return {
|
||||
'connections': {(CONNECTION_ZIGBEE, serial)},
|
||||
'identifiers': {(DECONZ_DOMAIN, serial)},
|
||||
|
||||
@@ -49,10 +49,6 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the FFmpeg binary motion sensor."""
|
||||
manager = hass.data[DATA_FFMPEG]
|
||||
|
||||
if not await manager.async_run_test(config.get(CONF_INPUT)):
|
||||
return
|
||||
|
||||
entity = FFmpegMotion(hass, manager, config)
|
||||
async_add_entities([entity])
|
||||
|
||||
|
||||
@@ -46,10 +46,6 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the FFmpeg noise binary sensor."""
|
||||
manager = hass.data[DATA_FFMPEG]
|
||||
|
||||
if not await manager.async_run_test(config.get(CONF_INPUT)):
|
||||
return
|
||||
|
||||
entity = FFmpegNoise(hass, manager, config)
|
||||
async_add_entities([entity])
|
||||
|
||||
|
||||
74
homeassistant/components/binary_sensor/fibaro.py
Normal file
74
homeassistant/components/binary_sensor/fibaro.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Support for Fibaro binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.fibaro/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, ENTITY_ID_FORMAT)
|
||||
from homeassistant.components.fibaro import (
|
||||
FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice)
|
||||
|
||||
DEPENDENCIES = ['fibaro']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSOR_TYPES = {
|
||||
'com.fibaro.doorSensor': ['Door', 'mdi:window-open', 'door'],
|
||||
'com.fibaro.windowSensor': ['Window', 'mdi:window-open', 'window'],
|
||||
'com.fibaro.smokeSensor': ['Smoke', 'mdi:smoking', 'smoke'],
|
||||
'com.fibaro.FGMS001': ['Motion', 'mdi:run', 'motion'],
|
||||
'com.fibaro.heatDetector': ['Heat', 'mdi:fire', 'heat'],
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Perform the setup for Fibaro controller devices."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
add_entities(
|
||||
[FibaroBinarySensor(device, hass.data[FIBARO_CONTROLLER])
|
||||
for device in hass.data[FIBARO_DEVICES]['binary_sensor']], True)
|
||||
|
||||
|
||||
class FibaroBinarySensor(FibaroDevice, BinarySensorDevice):
|
||||
"""Representation of a Fibaro Binary Sensor."""
|
||||
|
||||
def __init__(self, fibaro_device, controller):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._state = None
|
||||
super().__init__(fibaro_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
|
||||
stype = None
|
||||
if fibaro_device.type in SENSOR_TYPES:
|
||||
stype = fibaro_device.type
|
||||
elif fibaro_device.baseType in SENSOR_TYPES:
|
||||
stype = fibaro_device.baseType
|
||||
if stype:
|
||||
self._device_class = SENSOR_TYPES[stype][2]
|
||||
self._icon = SENSOR_TYPES[stype][1]
|
||||
else:
|
||||
self._device_class = None
|
||||
self._icon = None
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class of the sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and update the state."""
|
||||
self._state = self.current_binary_state
|
||||
@@ -57,7 +57,8 @@ class InsteonBinarySensor(InsteonEntity, BinarySensorDevice):
|
||||
"""Return the boolean response if the node is on."""
|
||||
on_val = bool(self._insteon_device_state.value)
|
||||
|
||||
if self._insteon_device_state.name == 'lightSensor':
|
||||
if self._insteon_device_state.name in ['lightSensor',
|
||||
'openClosedSensor']:
|
||||
return not on_val
|
||||
|
||||
return on_val
|
||||
|
||||
53
homeassistant/components/binary_sensor/lupusec.py
Normal file
53
homeassistant/components/binary_sensor/lupusec.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
This component provides HA binary_sensor support for Lupusec Security System.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.lupusec/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.lupusec import (LupusecDevice,
|
||||
DOMAIN as LUPUSEC_DOMAIN)
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice,
|
||||
DEVICE_CLASSES)
|
||||
|
||||
DEPENDENCIES = ['lupusec']
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=2)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up a sensor for an Lupusec device."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
import lupupy.constants as CONST
|
||||
|
||||
data = hass.data[LUPUSEC_DOMAIN]
|
||||
|
||||
device_types = [CONST.TYPE_OPENING]
|
||||
|
||||
devices = []
|
||||
for device in data.lupusec.get_devices(generic_type=device_types):
|
||||
devices.append(LupusecBinarySensor(data, device))
|
||||
|
||||
add_entities(devices)
|
||||
|
||||
|
||||
class LupusecBinarySensor(LupusecDevice, BinarySensorDevice):
|
||||
"""A binary sensor implementation for Lupusec device."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
return self._device.is_on
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the binary sensor."""
|
||||
if self._device.generic_type not in DEVICE_CLASSES:
|
||||
return None
|
||||
return self._device.generic_type
|
||||
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.mqtt/
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -19,7 +18,8 @@ from homeassistant.const import (
|
||||
from homeassistant.components.mqtt import (
|
||||
ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC,
|
||||
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
|
||||
MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo)
|
||||
MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
||||
subscription)
|
||||
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@@ -79,21 +79,8 @@ async def _async_setup_entity(hass, config, async_add_entities,
|
||||
value_template.hass = hass
|
||||
|
||||
async_add_entities([MqttBinarySensor(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_AVAILABILITY_TOPIC),
|
||||
config.get(CONF_DEVICE_CLASS),
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_FORCE_UPDATE),
|
||||
config.get(CONF_OFF_DELAY),
|
||||
config.get(CONF_PAYLOAD_ON),
|
||||
config.get(CONF_PAYLOAD_OFF),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||
value_template,
|
||||
config.get(CONF_UNIQUE_ID),
|
||||
config.get(CONF_DEVICE),
|
||||
discovery_hash,
|
||||
config,
|
||||
discovery_hash
|
||||
)])
|
||||
|
||||
|
||||
@@ -101,37 +88,80 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
|
||||
MqttEntityDeviceInfo, BinarySensorDevice):
|
||||
"""Representation a binary sensor that is updated by MQTT."""
|
||||
|
||||
def __init__(self, name, state_topic, availability_topic, device_class,
|
||||
qos, force_update, off_delay, payload_on, payload_off,
|
||||
payload_available, payload_not_available, value_template,
|
||||
unique_id: Optional[str], device_config: Optional[ConfigType],
|
||||
discovery_hash):
|
||||
def __init__(self, config, discovery_hash):
|
||||
"""Initialize the MQTT binary sensor."""
|
||||
MqttAvailability.__init__(self, availability_topic, qos,
|
||||
payload_available, payload_not_available)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash)
|
||||
MqttEntityDeviceInfo.__init__(self, device_config)
|
||||
self._name = name
|
||||
self._config = config
|
||||
self._state = None
|
||||
self._state_topic = state_topic
|
||||
self._device_class = device_class
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
self._qos = qos
|
||||
self._force_update = force_update
|
||||
self._off_delay = off_delay
|
||||
self._template = value_template
|
||||
self._unique_id = unique_id
|
||||
self._discovery_hash = discovery_hash
|
||||
self._sub_state = None
|
||||
self._delay_listener = None
|
||||
|
||||
self._name = None
|
||||
self._state_topic = None
|
||||
self._device_class = None
|
||||
self._payload_on = None
|
||||
self._payload_off = None
|
||||
self._qos = None
|
||||
self._force_update = None
|
||||
self._off_delay = None
|
||||
self._template = None
|
||||
self._unique_id = None
|
||||
|
||||
# Load config
|
||||
self._setup_from_config(config)
|
||||
|
||||
availability_topic = config.get(CONF_AVAILABILITY_TOPIC)
|
||||
payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
|
||||
payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE)
|
||||
device_config = config.get(CONF_DEVICE)
|
||||
|
||||
MqttAvailability.__init__(self, availability_topic, self._qos,
|
||||
payload_available, payload_not_available)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash,
|
||||
self.discovery_update)
|
||||
MqttEntityDeviceInfo.__init__(self, device_config)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events."""
|
||||
await MqttAvailability.async_added_to_hass(self)
|
||||
await MqttDiscoveryUpdate.async_added_to_hass(self)
|
||||
await self._subscribe_topics()
|
||||
|
||||
async def discovery_update(self, discovery_payload):
|
||||
"""Handle updated discovery message."""
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
self._setup_from_config(config)
|
||||
await self.availability_discovery_update(config)
|
||||
await self._subscribe_topics()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def _setup_from_config(self, config):
|
||||
"""(Re)Setup the entity."""
|
||||
self._name = config.get(CONF_NAME)
|
||||
self._state_topic = config.get(CONF_STATE_TOPIC)
|
||||
self._device_class = config.get(CONF_DEVICE_CLASS)
|
||||
self._qos = config.get(CONF_QOS)
|
||||
self._force_update = config.get(CONF_FORCE_UPDATE)
|
||||
self._off_delay = config.get(CONF_OFF_DELAY)
|
||||
self._payload_on = config.get(CONF_PAYLOAD_ON)
|
||||
self._payload_off = config.get(CONF_PAYLOAD_OFF)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None and value_template.hass is None:
|
||||
value_template.hass = self.hass
|
||||
self._template = value_template
|
||||
|
||||
self._unique_id = config.get(CONF_UNIQUE_ID)
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
@callback
|
||||
def off_delay_listener(now):
|
||||
"""Switch device off after a delay."""
|
||||
self._delay_listener = None
|
||||
self._state = False
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
def state_message_received(topic, payload, qos):
|
||||
def state_message_received(_topic, payload, _qos):
|
||||
"""Handle a new received MQTT state message."""
|
||||
if self._template is not None:
|
||||
payload = self._template.async_render_with_possible_json_value(
|
||||
@@ -146,24 +176,26 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
|
||||
self._name, self._state_topic)
|
||||
return
|
||||
|
||||
if self._delay_listener is not None:
|
||||
self._delay_listener()
|
||||
self._delay_listener = None
|
||||
|
||||
if (self._state and self._off_delay is not None):
|
||||
@callback
|
||||
def off_delay_listener(now):
|
||||
"""Switch device off after a delay."""
|
||||
self._delay_listener = None
|
||||
self._state = False
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._delay_listener is not None:
|
||||
self._delay_listener()
|
||||
|
||||
self._delay_listener = evt.async_call_later(
|
||||
self.hass, self._off_delay, off_delay_listener)
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._state_topic, state_message_received, self._qos)
|
||||
self._sub_state = await subscription.async_subscribe_topics(
|
||||
self.hass, self._sub_state,
|
||||
{'state_topic': {'topic': self._state_topic,
|
||||
'msg_callback': state_message_received,
|
||||
'qos': self._qos}})
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Unsubscribe when removed."""
|
||||
await subscription.async_unsubscribe_topics(self.hass, self._sub_state)
|
||||
await MqttAvailability.async_will_remove_from_hass(self)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
||||
@@ -37,8 +37,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_VARIABLE): cv.string,
|
||||
vol.Required(CONF_PAYLOAD): vol.Schema(dict),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default='on'): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default='off'): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default='on'): vol.Any(
|
||||
cv.positive_int, cv.small_float, cv.string),
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default='off'): vol.Any(
|
||||
cv.positive_int, cv.small_float, cv.string),
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER, default=False): cv.boolean,
|
||||
vol.Optional(CONF_RESET_DELAY_SEC, default=30): cv.positive_int
|
||||
})
|
||||
|
||||
103
homeassistant/components/binary_sensor/point.py
Normal file
103
homeassistant/components/binary_sensor/point.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
Support for Minut Point.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.point/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.point import MinutPointEntity
|
||||
from homeassistant.components.point.const import (
|
||||
DOMAIN as POINT_DOMAIN, NEW_DEVICE, SIGNAL_WEBHOOK)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
EVENTS = {
|
||||
'battery': # On means low, Off means normal
|
||||
('battery_low', ''),
|
||||
'button_press': # On means the button was pressed, Off means normal
|
||||
('short_button_press', ''),
|
||||
'cold': # On means cold, Off means normal
|
||||
('temperature_low', 'temperature_risen_normal'),
|
||||
'connectivity': # On means connected, Off means disconnected
|
||||
('device_online', 'device_offline'),
|
||||
'dry': # On means too dry, Off means normal
|
||||
('humidity_low', 'humidity_risen_normal'),
|
||||
'heat': # On means hot, Off means normal
|
||||
('temperature_high', 'temperature_dropped_normal'),
|
||||
'moisture': # On means wet, Off means dry
|
||||
('humidity_high', 'humidity_dropped_normal'),
|
||||
'sound': # On means sound detected, Off means no sound (clear)
|
||||
('avg_sound_high', 'sound_level_dropped_normal'),
|
||||
'tamper': # On means the point was removed or attached
|
||||
('tamper', ''),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up a Point's binary sensors based on a config entry."""
|
||||
device_id = config_entry.data[NEW_DEVICE]
|
||||
client = hass.data[POINT_DOMAIN][config_entry.entry_id]
|
||||
async_add_entities((MinutPointBinarySensor(client, device_id, device_class)
|
||||
for device_class in EVENTS), True)
|
||||
|
||||
|
||||
class MinutPointBinarySensor(MinutPointEntity):
|
||||
"""The platform class required by Home Assistant."""
|
||||
|
||||
def __init__(self, point_client, device_id, device_class):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(point_client, device_id, device_class)
|
||||
|
||||
self._async_unsub_hook_dispatcher_connect = None
|
||||
self._events = EVENTS[device_class]
|
||||
self._is_on = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self._async_unsub_hook_dispatcher_connect = async_dispatcher_connect(
|
||||
self.hass, SIGNAL_WEBHOOK, self._webhook_event)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect dispatcher listener when removed."""
|
||||
await super().async_will_remove_from_hass()
|
||||
if self._async_unsub_hook_dispatcher_connect:
|
||||
self._async_unsub_hook_dispatcher_connect()
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Update the value of the sensor."""
|
||||
if not self.is_updated:
|
||||
return
|
||||
if self._events[0] in self.device.ongoing_events:
|
||||
self._is_on = True
|
||||
else:
|
||||
self._is_on = None
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
def _webhook_event(self, data, webhook):
|
||||
"""Process new event from the webhook."""
|
||||
if self.device.webhook != webhook:
|
||||
return
|
||||
_type = data.get('event', {}).get('type')
|
||||
if _type not in self._events:
|
||||
return
|
||||
_LOGGER.debug("Recieved webhook: %s", _type)
|
||||
if _type == self._events[0]:
|
||||
self._is_on = True
|
||||
if _type == self._events[1]:
|
||||
self._is_on = None
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
if self.device_class == 'connectivity':
|
||||
# connectivity is the other way around.
|
||||
return not self._is_on
|
||||
return self._is_on
|
||||
@@ -8,28 +8,29 @@ import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.rainmachine import (
|
||||
BINARY_SENSORS, DATA_RAINMACHINE, SENSOR_UPDATE_TOPIC, TYPE_FREEZE,
|
||||
TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS, TYPE_HOURLY, TYPE_MONTH,
|
||||
TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY, RainMachineEntity)
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
BINARY_SENSORS, DATA_CLIENT, DOMAIN as RAINMACHINE_DOMAIN,
|
||||
SENSOR_UPDATE_TOPIC, TYPE_FREEZE, TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS,
|
||||
TYPE_HOURLY, TYPE_MONTH, TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY,
|
||||
RainMachineEntity)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
DEPENDENCIES = ['rainmachine']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the RainMachine Switch platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
"""Set up RainMachine binary sensors based on the old way."""
|
||||
pass
|
||||
|
||||
rainmachine = hass.data[DATA_RAINMACHINE]
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up RainMachine binary sensors based on a config entry."""
|
||||
rainmachine = hass.data[RAINMACHINE_DOMAIN][DATA_CLIENT][entry.entry_id]
|
||||
|
||||
binary_sensors = []
|
||||
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
|
||||
for sensor_type in rainmachine.binary_sensor_conditions:
|
||||
name, icon = BINARY_SENSORS[sensor_type]
|
||||
binary_sensors.append(
|
||||
RainMachineBinarySensor(rainmachine, sensor_type, name, icon))
|
||||
@@ -70,15 +71,15 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice):
|
||||
return '{0}_{1}'.format(
|
||||
self.rainmachine.device_mac.replace(':', ''), self._sensor_type)
|
||||
|
||||
@callback
|
||||
def _update_data(self):
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SENSOR_UPDATE_TOPIC, self._update_data)
|
||||
@callback
|
||||
def update(self):
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
self._dispatcher_handlers.append(async_dispatcher_connect(
|
||||
self.hass, SENSOR_UPDATE_TOPIC, update))
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the state."""
|
||||
|
||||
117
homeassistant/components/binary_sensor/sense.py
Normal file
117
homeassistant/components/binary_sensor/sense.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
Support for monitoring a Sense energy sensor device.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.sense/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sense import SENSE_DATA
|
||||
|
||||
DEPENDENCIES = ['sense']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BIN_SENSOR_CLASS = 'power'
|
||||
MDI_ICONS = {'ac': 'air-conditioner',
|
||||
'aquarium': 'fish',
|
||||
'car': 'car-electric',
|
||||
'computer': 'desktop-classic',
|
||||
'cup': 'coffee',
|
||||
'dehumidifier': 'water-off',
|
||||
'dishes': 'dishwasher',
|
||||
'drill': 'toolbox',
|
||||
'fan': 'fan',
|
||||
'freezer': 'fridge-top',
|
||||
'fridge': 'fridge-bottom',
|
||||
'game': 'gamepad-variant',
|
||||
'garage': 'garage',
|
||||
'grill': 'stove',
|
||||
'heat': 'fire',
|
||||
'heater': 'radiatior',
|
||||
'humidifier': 'water',
|
||||
'kettle': 'kettle',
|
||||
'leafblower': 'leaf',
|
||||
'lightbulb': 'lightbulb',
|
||||
'media_console': 'set-top-box',
|
||||
'modem': 'router-wireless',
|
||||
'outlet': 'power-socket-us',
|
||||
'papershredder': 'shredder',
|
||||
'printer': 'printer',
|
||||
'pump': 'water-pump',
|
||||
'settings': 'settings',
|
||||
'skillet': 'pot',
|
||||
'smartcamera': 'webcam',
|
||||
'socket': 'power-plug',
|
||||
'sound': 'speaker',
|
||||
'stove': 'stove',
|
||||
'trash': 'trash-can',
|
||||
'tv': 'television',
|
||||
'vacuum': 'robot-vacuum',
|
||||
'washer': 'washing-machine'}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Sense sensor."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
data = hass.data[SENSE_DATA]
|
||||
|
||||
sense_devices = data.get_discovered_device_data()
|
||||
devices = [SenseDevice(data, device) for device in sense_devices
|
||||
if device['tags']['DeviceListAllowed'] == 'true']
|
||||
add_entities(devices)
|
||||
|
||||
|
||||
def sense_to_mdi(sense_icon):
|
||||
"""Convert sense icon to mdi icon."""
|
||||
return 'mdi:' + MDI_ICONS.get(sense_icon, 'power-plug')
|
||||
|
||||
|
||||
class SenseDevice(BinarySensorDevice):
|
||||
"""Implementation of a Sense energy device binary sensor."""
|
||||
|
||||
def __init__(self, data, device):
|
||||
"""Initialize the sensor."""
|
||||
self._name = device['name']
|
||||
self._id = device['id']
|
||||
self._icon = sense_to_mdi(device['icon'])
|
||||
self._data = data
|
||||
self._state = False
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the id of the binary sensor."""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of the binary sensor."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class of the binary sensor."""
|
||||
return BIN_SENSOR_CLASS
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
from sense_energy.sense_api import SenseAPITimeoutException
|
||||
try:
|
||||
self._data.get_realtime()
|
||||
except SenseAPITimeoutException:
|
||||
_LOGGER.error("Timeout retrieving data")
|
||||
return
|
||||
self._state = self._name in self._data.active_devices
|
||||
@@ -15,7 +15,7 @@ from homeassistant.components.binary_sensor import (
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE,
|
||||
CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE,
|
||||
CONF_SENSORS, CONF_DEVICE_CLASS, EVENT_HOMEASSISTANT_START)
|
||||
CONF_SENSORS, CONF_DEVICE_CLASS, EVENT_HOMEASSISTANT_START, MATCH_ALL)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
@@ -55,22 +55,49 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
icon_template = device_config.get(CONF_ICON_TEMPLATE)
|
||||
entity_picture_template = device_config.get(
|
||||
CONF_ENTITY_PICTURE_TEMPLATE)
|
||||
entity_ids = (device_config.get(ATTR_ENTITY_ID) or
|
||||
value_template.extract_entities())
|
||||
entity_ids = set()
|
||||
manual_entity_ids = device_config.get(ATTR_ENTITY_ID)
|
||||
|
||||
invalid_templates = []
|
||||
|
||||
for tpl_name, template in (
|
||||
(CONF_VALUE_TEMPLATE, value_template),
|
||||
(CONF_ICON_TEMPLATE, icon_template),
|
||||
(CONF_ENTITY_PICTURE_TEMPLATE, entity_picture_template),
|
||||
):
|
||||
if template is None:
|
||||
continue
|
||||
template.hass = hass
|
||||
|
||||
if manual_entity_ids is not None:
|
||||
continue
|
||||
|
||||
template_entity_ids = template.extract_entities()
|
||||
if template_entity_ids == MATCH_ALL:
|
||||
entity_ids = MATCH_ALL
|
||||
# Cut off _template from name
|
||||
invalid_templates.append(tpl_name[:-9])
|
||||
elif entity_ids != MATCH_ALL:
|
||||
entity_ids |= set(template_entity_ids)
|
||||
|
||||
if manual_entity_ids is not None:
|
||||
entity_ids = manual_entity_ids
|
||||
elif entity_ids != MATCH_ALL:
|
||||
entity_ids = list(entity_ids)
|
||||
|
||||
if invalid_templates:
|
||||
_LOGGER.warning(
|
||||
'Template binary sensor %s has no entity ids configured to'
|
||||
' track nor were we able to extract the entities to track'
|
||||
' from the %s template(s). This entity will only be able'
|
||||
' to be updated manually.',
|
||||
device, ', '.join(invalid_templates))
|
||||
|
||||
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
|
||||
device_class = device_config.get(CONF_DEVICE_CLASS)
|
||||
delay_on = device_config.get(CONF_DELAY_ON)
|
||||
delay_off = device_config.get(CONF_DELAY_OFF)
|
||||
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
if icon_template is not None:
|
||||
icon_template.hass = hass
|
||||
|
||||
if entity_picture_template is not None:
|
||||
entity_picture_template.hass = hass
|
||||
|
||||
sensors.append(
|
||||
BinarySensorTemplate(
|
||||
hass, device, friendly_name, device_class, value_template,
|
||||
@@ -117,10 +144,12 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
@callback
|
||||
def template_bsensor_startup(event):
|
||||
"""Update template on startup."""
|
||||
async_track_state_change(
|
||||
self.hass, self._entities, template_bsensor_state_listener)
|
||||
if self._entities != MATCH_ALL:
|
||||
# Track state change only for valid templates
|
||||
async_track_state_change(
|
||||
self.hass, self._entities, template_bsensor_state_listener)
|
||||
|
||||
self.hass.async_add_job(self.async_check_state)
|
||||
self.async_check_state()
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, template_bsensor_startup)
|
||||
@@ -218,3 +247,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
async_track_same_state(
|
||||
self.hass, period, set_state, entity_ids=self._entities,
|
||||
async_check_same_func=lambda *args: self._async_render() == state)
|
||||
|
||||
async def async_update(self):
|
||||
"""Force update of the state from the template."""
|
||||
self.async_check_state()
|
||||
|
||||
@@ -15,14 +15,14 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, CONF_DEVICE_CLASS, CONF_ENTITY_ID,
|
||||
CONF_FRIENDLY_NAME, STATE_UNKNOWN)
|
||||
CONF_FRIENDLY_NAME, STATE_UNKNOWN, CONF_SENSORS)
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
REQUIREMENTS = ['numpy==1.15.2']
|
||||
REQUIREMENTS = ['numpy==1.15.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,7 +38,6 @@ CONF_INVERT = 'invert'
|
||||
CONF_MAX_SAMPLES = 'max_samples'
|
||||
CONF_MIN_GRADIENT = 'min_gradient'
|
||||
CONF_SAMPLE_DURATION = 'sample_duration'
|
||||
CONF_SENSORS = 'sensors'
|
||||
|
||||
SENSOR_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
@@ -78,9 +77,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
)
|
||||
if not sensors:
|
||||
_LOGGER.error("No sensors added")
|
||||
return False
|
||||
return
|
||||
add_entities(sensors)
|
||||
return True
|
||||
|
||||
|
||||
class SensorTrend(BinarySensorDevice):
|
||||
|
||||
132
homeassistant/components/binary_sensor/w800rf32.py
Normal file
132
homeassistant/components/binary_sensor/w800rf32.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
Support for w800rf32 binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.w800rf32/
|
||||
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.components.w800rf32 import (W800RF32_DEVICE)
|
||||
from homeassistant.const import (CONF_DEVICE_CLASS, CONF_NAME, CONF_DEVICES)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import event as evt
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.helpers.dispatcher import (async_dispatcher_connect)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['w800rf32']
|
||||
CONF_OFF_DELAY = 'off_delay'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_DEVICES): {
|
||||
cv.string: vol.Schema({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_OFF_DELAY):
|
||||
vol.All(cv.time_period, cv.positive_timedelta)
|
||||
})
|
||||
},
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config,
|
||||
add_entities, discovery_info=None):
|
||||
"""Set up the Binary Sensor platform to w800rf32."""
|
||||
binary_sensors = []
|
||||
# device_id --> "c1 or a3" X10 device. entity (type dictionary)
|
||||
# --> name, device_class etc
|
||||
for device_id, entity in config[CONF_DEVICES].items():
|
||||
|
||||
_LOGGER.debug("Add %s w800rf32.binary_sensor (class %s)",
|
||||
entity[CONF_NAME], entity.get(CONF_DEVICE_CLASS))
|
||||
|
||||
device = W800rf32BinarySensor(
|
||||
device_id, entity.get(CONF_NAME), entity.get(CONF_DEVICE_CLASS),
|
||||
entity.get(CONF_OFF_DELAY))
|
||||
|
||||
binary_sensors.append(device)
|
||||
|
||||
add_entities(binary_sensors)
|
||||
|
||||
|
||||
class W800rf32BinarySensor(BinarySensorDevice):
|
||||
"""A representation of a w800rf32 binary sensor."""
|
||||
|
||||
def __init__(self, device_id, name, device_class=None, off_delay=None):
|
||||
"""Initialize the w800rf32 sensor."""
|
||||
self._signal = W800RF32_DEVICE.format(device_id)
|
||||
self._name = name
|
||||
self._device_class = device_class
|
||||
self._off_delay = off_delay
|
||||
self._state = False
|
||||
self._delay_listener = None
|
||||
|
||||
@callback
|
||||
def _off_delay_listener(self, now):
|
||||
"""Switch device off after a delay."""
|
||||
self._delay_listener = None
|
||||
self.update_state(False)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the device name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the sensor class."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the sensor state is True."""
|
||||
return self._state
|
||||
|
||||
@callback
|
||||
def binary_sensor_update(self, event):
|
||||
"""Call for control updates from the w800rf32 gateway."""
|
||||
import W800rf32 as w800rf32mod
|
||||
|
||||
if not isinstance(event, w800rf32mod.W800rf32Event):
|
||||
return
|
||||
|
||||
dev_id = event.device
|
||||
command = event.command
|
||||
|
||||
_LOGGER.debug(
|
||||
"BinarySensor update (Device ID: %s Command %s ...)",
|
||||
dev_id, command)
|
||||
|
||||
# Update the w800rf32 device state
|
||||
if command in ('On', 'Off'):
|
||||
is_on = command == 'On'
|
||||
self.update_state(is_on)
|
||||
|
||||
if (self.is_on and self._off_delay is not None and
|
||||
self._delay_listener is None):
|
||||
|
||||
self._delay_listener = evt.async_track_point_in_time(
|
||||
self.hass, self._off_delay_listener,
|
||||
dt_util.utcnow() + self._off_delay)
|
||||
|
||||
def update_state(self, state):
|
||||
"""Update the state of the device."""
|
||||
self._state = state
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register update callback."""
|
||||
async_dispatcher_connect(self.hass, self._signal,
|
||||
self.binary_sensor_update)
|
||||
@@ -209,7 +209,7 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
|
||||
else:
|
||||
self._should_poll = True
|
||||
if self.entity_id is not None:
|
||||
self._hass.bus.fire('motion', {
|
||||
self._hass.bus.fire('xiaomi_aqara.motion', {
|
||||
'entity_id': self.entity_id
|
||||
})
|
||||
|
||||
@@ -357,6 +357,9 @@ class XiaomiVibration(XiaomiBinarySensor):
|
||||
def parse_data(self, data, raw_data):
|
||||
"""Parse data sent by gateway."""
|
||||
value = data.get(self._data_key)
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
if value not in ('vibrate', 'tilt', 'free_fall'):
|
||||
_LOGGER.warning("Unsupported movement_type detected: %s",
|
||||
value)
|
||||
@@ -414,7 +417,7 @@ class XiaomiButton(XiaomiBinarySensor):
|
||||
_LOGGER.warning("Unsupported click_type detected: %s", value)
|
||||
return False
|
||||
|
||||
self._hass.bus.fire('click', {
|
||||
self._hass.bus.fire('xiaomi_aqara.click', {
|
||||
'entity_id': self.entity_id,
|
||||
'click_type': click_type
|
||||
})
|
||||
@@ -450,14 +453,14 @@ class XiaomiCube(XiaomiBinarySensor):
|
||||
def parse_data(self, data, raw_data):
|
||||
"""Parse data sent by gateway."""
|
||||
if self._data_key in data:
|
||||
self._hass.bus.fire('cube_action', {
|
||||
self._hass.bus.fire('xiaomi_aqara.cube_action', {
|
||||
'entity_id': self.entity_id,
|
||||
'action_type': data[self._data_key]
|
||||
})
|
||||
self._last_action = data[self._data_key]
|
||||
|
||||
if 'rotate' in data:
|
||||
self._hass.bus.fire('cube_action', {
|
||||
self._hass.bus.fire('xiaomi_aqara.cube_action', {
|
||||
'entity_id': self.entity_id,
|
||||
'action_type': 'rotate',
|
||||
'action_value': float(data['rotate'].replace(",", "."))
|
||||
|
||||
@@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['zha']
|
||||
|
||||
# ZigBee Cluster Library Zone Type to Home Assistant device class
|
||||
# Zigbee Cluster Library Zone Type to Home Assistant device class
|
||||
CLASS_MAPPING = {
|
||||
0x000d: 'motion',
|
||||
0x0015: 'opening',
|
||||
@@ -145,7 +145,7 @@ class Remote(zha.Entity, BinarySensorDevice):
|
||||
_domain = DOMAIN
|
||||
|
||||
class OnOffListener:
|
||||
"""Listener for the OnOff ZigBee cluster."""
|
||||
"""Listener for the OnOff Zigbee cluster."""
|
||||
|
||||
def __init__(self, entity):
|
||||
"""Initialize OnOffListener."""
|
||||
@@ -170,7 +170,7 @@ class Remote(zha.Entity, BinarySensorDevice):
|
||||
pass
|
||||
|
||||
class LevelListener:
|
||||
"""Listener for the LevelControl ZigBee cluster."""
|
||||
"""Listener for the LevelControl Zigbee cluster."""
|
||||
|
||||
def __init__(self, entity):
|
||||
"""Initialize LevelListener."""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Contains functionality to use a ZigBee device as a binary sensor.
|
||||
Contains functionality to use a Zigbee device as a binary sensor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.zigbee/
|
||||
@@ -23,7 +23,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the ZigBee binary sensor platform."""
|
||||
"""Set up the Zigbee binary sensor platform."""
|
||||
add_entities(
|
||||
[ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))], True)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
|
||||
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
|
||||
|
||||
REQUIREMENTS = ['blinkpy==0.10.1']
|
||||
REQUIREMENTS = ['blinkpy==0.10.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ class BloomSky:
|
||||
"""Handle all communication with the BloomSky API."""
|
||||
|
||||
# API documentation at http://weatherlution.com/bloomsky-api/
|
||||
API_URL = 'https://api.bloomsky.com/api/skydata'
|
||||
API_URL = 'http://api.bloomsky.com/api/skydata'
|
||||
|
||||
def __init__(self, api_key):
|
||||
"""Initialize the BookSky."""
|
||||
|
||||
@@ -299,7 +299,8 @@ class Camera(Entity):
|
||||
a direct stream from the camera.
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
await self.handle_async_still_stream(request, self.frame_interval)
|
||||
return await self.handle_async_still_stream(
|
||||
request, self.frame_interval)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -59,8 +59,7 @@ class AmcrestCam(Camera):
|
||||
"""Return an MJPEG stream."""
|
||||
# The snapshot implementation is handled by the parent class
|
||||
if self._stream_source == STREAM_SOURCE_LIST['snapshot']:
|
||||
await super().handle_async_mjpeg_stream(request)
|
||||
return
|
||||
return await super().handle_async_mjpeg_stream(request)
|
||||
|
||||
if self._stream_source == STREAM_SOURCE_LIST['mjpeg']:
|
||||
# stream an MJPEG image stream directly from the camera
|
||||
@@ -69,20 +68,22 @@ class AmcrestCam(Camera):
|
||||
stream_coro = websession.get(
|
||||
streaming_url, auth=self._token, timeout=TIMEOUT)
|
||||
|
||||
await async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
return await async_aiohttp_proxy_web(
|
||||
self.hass, request, stream_coro)
|
||||
|
||||
else:
|
||||
# streaming via fmpeg
|
||||
from haffmpeg import CameraMjpeg
|
||||
# streaming via ffmpeg
|
||||
from haffmpeg import CameraMjpeg
|
||||
|
||||
streaming_url = self._camera.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)
|
||||
streaming_url = self._camera.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)
|
||||
|
||||
await async_aiohttp_proxy_stream(
|
||||
try:
|
||||
return await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
finally:
|
||||
await stream.close()
|
||||
|
||||
@property
|
||||
|
||||
@@ -101,10 +101,12 @@ class ArloCam(Camera):
|
||||
await stream.open_camera(
|
||||
video.video_url, extra_cmd=self._ffmpeg_arguments)
|
||||
|
||||
await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
await stream.close()
|
||||
try:
|
||||
return await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
finally:
|
||||
await stream.close()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -98,10 +98,12 @@ class CanaryCamera(Camera):
|
||||
self._live_stream_session.live_stream_url,
|
||||
extra_cmd=self._ffmpeg_arguments)
|
||||
|
||||
await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
await stream.close()
|
||||
try:
|
||||
return await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
finally:
|
||||
await stream.close()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SESSION_RENEW)
|
||||
def renew_live_stream_session(self):
|
||||
|
||||
@@ -32,8 +32,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up a FFmpeg camera."""
|
||||
if not await hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)):
|
||||
return
|
||||
async_add_entities([FFmpegCamera(hass, config)])
|
||||
|
||||
|
||||
|
||||
@@ -134,8 +134,7 @@ class MjpegCamera(Camera):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
# aiohttp don't support DigestAuth -> Fallback
|
||||
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||
await super().handle_async_mjpeg_stream(request)
|
||||
return
|
||||
return await super().handle_async_mjpeg_stream(request)
|
||||
|
||||
# connect to stream
|
||||
websession = async_get_clientsession(self.hass)
|
||||
|
||||
@@ -89,13 +89,12 @@ class MqttCamera(Camera):
|
||||
"""Return a unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe MQTT events."""
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""Handle new MQTT messages."""
|
||||
self._last_image = payload
|
||||
|
||||
return mqtt.async_subscribe(
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic, message_received, self._qos, None)
|
||||
|
||||
@@ -74,9 +74,6 @@ SERVICE_PTZ_SCHEMA = vol.Schema({
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up a ONVIF camera."""
|
||||
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_HOST)):
|
||||
return
|
||||
|
||||
def handle_ptz(service):
|
||||
"""Handle PTZ service call."""
|
||||
pan = service.data.get(ATTR_PAN, None)
|
||||
@@ -93,8 +90,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
for camera in target_cameras:
|
||||
camera.perform_ptz(pan, tilt, zoom)
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_PTZ, handle_ptz,
|
||||
schema=SERVICE_PTZ_SCHEMA)
|
||||
hass.services.register(DOMAIN, SERVICE_PTZ, handle_ptz,
|
||||
schema=SERVICE_PTZ_SCHEMA)
|
||||
add_entities([ONVIFHassCamera(hass, config)])
|
||||
|
||||
|
||||
@@ -221,10 +218,12 @@ class ONVIFHassCamera(Camera):
|
||||
await stream.open_camera(
|
||||
self._input, extra_cmd=self._ffmpeg_arguments)
|
||||
|
||||
await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
await stream.close()
|
||||
try:
|
||||
return await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
finally:
|
||||
await stream.close()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -139,10 +139,12 @@ class RingCam(Camera):
|
||||
await stream.open_camera(
|
||||
self._video_url, extra_cmd=self._ffmpeg_arguments)
|
||||
|
||||
await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
await stream.close()
|
||||
try:
|
||||
return await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
finally:
|
||||
await stream.close()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
||||
@@ -92,7 +92,7 @@ class SynologyCamera(Camera):
|
||||
websession = async_get_clientsession(self.hass, self._verify_ssl)
|
||||
stream_coro = websession.get(streaming_url)
|
||||
|
||||
await async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
return await async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -158,7 +158,9 @@ class XiaomiCamera(Camera):
|
||||
await stream.open_camera(
|
||||
self._last_url, extra_cmd=self._extra_arguments)
|
||||
|
||||
await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
await stream.close()
|
||||
try:
|
||||
return await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
finally:
|
||||
await stream.close()
|
||||
|
||||
@@ -144,7 +144,9 @@ class YiCamera(Camera):
|
||||
await stream.open_camera(
|
||||
self._last_url, extra_cmd=self._extra_arguments)
|
||||
|
||||
await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
await stream.close()
|
||||
try:
|
||||
return await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
finally:
|
||||
await stream.close()
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "No se encontraron dispositivos de Google Cast en la red.",
|
||||
"single_instance_allowed": "S\u00f3lo es necesaria una \u00fanica configuraci\u00f3n de Google Cast."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "\u00bfQuieres configurar Google Cast?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
@@ -249,9 +249,11 @@ class ClimateDevice(Entity):
|
||||
self.hass, self.target_temperature_low, self.temperature_unit,
|
||||
self.precision)
|
||||
|
||||
if self.current_humidity is not None:
|
||||
data[ATTR_CURRENT_HUMIDITY] = self.current_humidity
|
||||
|
||||
if supported_features & SUPPORT_TARGET_HUMIDITY:
|
||||
data[ATTR_HUMIDITY] = self.target_humidity
|
||||
data[ATTR_CURRENT_HUMIDITY] = self.current_humidity
|
||||
|
||||
if supported_features & SUPPORT_TARGET_HUMIDITY_LOW:
|
||||
data[ATTR_MIN_HUMIDITY] = self.min_humidity
|
||||
|
||||
@@ -22,7 +22,7 @@ from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, TEMP_CELSIUS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pydaikin==0.4']
|
||||
REQUIREMENTS = ['pydaikin==0.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -82,7 +82,6 @@ class DaikinClimate(ClimateDevice):
|
||||
from pydaikin import appliance
|
||||
|
||||
self._api = api
|
||||
self._force_refresh = False
|
||||
self._list = {
|
||||
ATTR_OPERATION_MODE: list(HA_STATE_TO_DAIKIN),
|
||||
ATTR_FAN_MODE: list(
|
||||
@@ -102,19 +101,11 @@ class DaikinClimate(ClimateDevice):
|
||||
self._supported_features = SUPPORT_TARGET_TEMPERATURE \
|
||||
| SUPPORT_OPERATION_MODE
|
||||
|
||||
daikin_attr = HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE]
|
||||
if self._api.device.values.get(daikin_attr) is not None:
|
||||
if self._api.device.support_fan_mode:
|
||||
self._supported_features |= SUPPORT_FAN_MODE
|
||||
else:
|
||||
# even devices without support must have a default valid value
|
||||
self._api.device.values[daikin_attr] = 'A'
|
||||
|
||||
daikin_attr = HA_ATTR_TO_DAIKIN[ATTR_SWING_MODE]
|
||||
if self._api.device.values.get(daikin_attr) is not None:
|
||||
if self._api.device.support_swing_mode:
|
||||
self._supported_features |= SUPPORT_SWING_MODE
|
||||
else:
|
||||
# even devices without support must have a default valid value
|
||||
self._api.device.values[daikin_attr] = '0'
|
||||
|
||||
def get(self, key):
|
||||
"""Retrieve device settings from API library cache."""
|
||||
@@ -174,8 +165,10 @@ class DaikinClimate(ClimateDevice):
|
||||
|
||||
daikin_attr = HA_ATTR_TO_DAIKIN.get(attr)
|
||||
if daikin_attr is not None:
|
||||
if value in self._list[attr]:
|
||||
if attr == ATTR_OPERATION_MODE:
|
||||
values[daikin_attr] = HA_STATE_TO_DAIKIN[value]
|
||||
elif value in self._list[attr]:
|
||||
values[daikin_attr] = value.lower()
|
||||
else:
|
||||
_LOGGER.error("Invalid value %s for %s", attr, value)
|
||||
|
||||
@@ -187,7 +180,6 @@ class DaikinClimate(ClimateDevice):
|
||||
_LOGGER.error("Invalid temperature %s", value)
|
||||
|
||||
if values:
|
||||
self._force_refresh = True
|
||||
self._api.device.set(values)
|
||||
|
||||
@property
|
||||
@@ -268,5 +260,4 @@ class DaikinClimate(ClimateDevice):
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._api.update(no_throttle=self._force_refresh)
|
||||
self._force_refresh = False
|
||||
self._api.update()
|
||||
|
||||
@@ -17,7 +17,8 @@ from homeassistant.components.climate import (
|
||||
SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_UNKNOWN)
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_UNKNOWN, PRECISION_HALVES,
|
||||
PRECISION_TENTHS, PRECISION_WHOLE)
|
||||
from homeassistant.helpers import condition
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_time_interval)
|
||||
@@ -43,6 +44,7 @@ CONF_HOT_TOLERANCE = 'hot_tolerance'
|
||||
CONF_KEEP_ALIVE = 'keep_alive'
|
||||
CONF_INITIAL_OPERATION_MODE = 'initial_operation_mode'
|
||||
CONF_AWAY_TEMP = 'away_temp'
|
||||
CONF_PRECISION = 'precision'
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
|
||||
SUPPORT_OPERATION_MODE)
|
||||
|
||||
@@ -63,7 +65,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_INITIAL_OPERATION_MODE):
|
||||
vol.In([STATE_AUTO, STATE_OFF]),
|
||||
vol.Optional(CONF_AWAY_TEMP): vol.Coerce(float)
|
||||
vol.Optional(CONF_AWAY_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_PRECISION): vol.In(
|
||||
[PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]),
|
||||
})
|
||||
|
||||
|
||||
@@ -83,11 +87,13 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
keep_alive = config.get(CONF_KEEP_ALIVE)
|
||||
initial_operation_mode = config.get(CONF_INITIAL_OPERATION_MODE)
|
||||
away_temp = config.get(CONF_AWAY_TEMP)
|
||||
precision = config.get(CONF_PRECISION)
|
||||
|
||||
async_add_entities([GenericThermostat(
|
||||
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
|
||||
target_temp, ac_mode, min_cycle_duration, cold_tolerance,
|
||||
hot_tolerance, keep_alive, initial_operation_mode, away_temp)])
|
||||
hot_tolerance, keep_alive, initial_operation_mode, away_temp,
|
||||
precision)])
|
||||
|
||||
|
||||
class GenericThermostat(ClimateDevice):
|
||||
@@ -96,7 +102,7 @@ class GenericThermostat(ClimateDevice):
|
||||
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
||||
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
|
||||
cold_tolerance, hot_tolerance, keep_alive,
|
||||
initial_operation_mode, away_temp):
|
||||
initial_operation_mode, away_temp, precision):
|
||||
"""Initialize the thermostat."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
@@ -109,6 +115,7 @@ class GenericThermostat(ClimateDevice):
|
||||
self._initial_operation_mode = initial_operation_mode
|
||||
self._saved_target_temp = target_temp if target_temp is not None \
|
||||
else away_temp
|
||||
self._temp_precision = precision
|
||||
if self.ac_mode:
|
||||
self._current_operation = STATE_COOL
|
||||
self._operation_list = [STATE_COOL, STATE_OFF]
|
||||
@@ -202,6 +209,13 @@ class GenericThermostat(ClimateDevice):
|
||||
"""Return the name of the thermostat."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"""Return the precision of the system."""
|
||||
if self._temp_precision is not None:
|
||||
return self._temp_precision
|
||||
return super().precision
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
@@ -232,11 +246,11 @@ class GenericThermostat(ClimateDevice):
|
||||
if operation_mode == STATE_HEAT:
|
||||
self._current_operation = STATE_HEAT
|
||||
self._enabled = True
|
||||
await self._async_control_heating()
|
||||
await self._async_control_heating(force=True)
|
||||
elif operation_mode == STATE_COOL:
|
||||
self._current_operation = STATE_COOL
|
||||
self._enabled = True
|
||||
await self._async_control_heating()
|
||||
await self._async_control_heating(force=True)
|
||||
elif operation_mode == STATE_OFF:
|
||||
self._current_operation = STATE_OFF
|
||||
self._enabled = False
|
||||
@@ -262,7 +276,7 @@ class GenericThermostat(ClimateDevice):
|
||||
if temperature is None:
|
||||
return
|
||||
self._target_temp = temperature
|
||||
await self._async_control_heating()
|
||||
await self._async_control_heating(force=True)
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
@@ -307,7 +321,7 @@ class GenericThermostat(ClimateDevice):
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("Unable to update from sensor: %s", ex)
|
||||
|
||||
async def _async_control_heating(self, time=None):
|
||||
async def _async_control_heating(self, time=None, force=False):
|
||||
"""Check if we need to turn heating on or off."""
|
||||
async with self._temp_lock:
|
||||
if not self._active and None not in (self._cur_temp,
|
||||
@@ -320,16 +334,21 @@ class GenericThermostat(ClimateDevice):
|
||||
if not self._active or not self._enabled:
|
||||
return
|
||||
|
||||
if self.min_cycle_duration:
|
||||
if self._is_device_active:
|
||||
current_state = STATE_ON
|
||||
else:
|
||||
current_state = STATE_OFF
|
||||
long_enough = condition.state(
|
||||
self.hass, self.heater_entity_id, current_state,
|
||||
self.min_cycle_duration)
|
||||
if not long_enough:
|
||||
return
|
||||
if not force and time is None:
|
||||
# If the `force` argument is True, we
|
||||
# ignore `min_cycle_duration`.
|
||||
# If the `time` argument is not none, we were invoked for
|
||||
# keep-alive purposes, and `min_cycle_duration` is irrelevant.
|
||||
if self.min_cycle_duration:
|
||||
if self._is_device_active:
|
||||
current_state = STATE_ON
|
||||
else:
|
||||
current_state = STATE_OFF
|
||||
long_enough = condition.state(
|
||||
self.hass, self.heater_entity_id, current_state,
|
||||
self.min_cycle_duration)
|
||||
if not long_enough:
|
||||
return
|
||||
|
||||
too_cold = \
|
||||
self._target_temp - self._cur_temp >= self._cold_tolerance
|
||||
@@ -380,15 +399,19 @@ class GenericThermostat(ClimateDevice):
|
||||
|
||||
async def async_turn_away_mode_on(self):
|
||||
"""Turn away mode on by setting it on away hold indefinitely."""
|
||||
if self._is_away:
|
||||
return
|
||||
self._is_away = True
|
||||
self._saved_target_temp = self._target_temp
|
||||
self._target_temp = self._away_temp
|
||||
await self._async_control_heating()
|
||||
await self._async_control_heating(force=True)
|
||||
await self.async_update_ha_state()
|
||||
|
||||
async def async_turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
if not self._is_away:
|
||||
return
|
||||
self._is_away = False
|
||||
self._target_temp = self._saved_target_temp
|
||||
await self._async_control_heating()
|
||||
await self._async_control_heating(force=True)
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@@ -7,17 +7,16 @@ https://home-assistant.io/components/climate.homematic/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
STATE_AUTO, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE,
|
||||
ClimateDevice)
|
||||
STATE_AUTO, STATE_MANUAL, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE, ClimateDevice)
|
||||
from homeassistant.components.homematic import (
|
||||
ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT, HMDevice)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, STATE_UNKNOWN, TEMP_CELSIUS
|
||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_MANUAL = 'manual'
|
||||
STATE_BOOST = 'boost'
|
||||
STATE_COMFORT = 'comfort'
|
||||
STATE_LOWERING = 'lowering'
|
||||
@@ -41,7 +40,7 @@ HM_HUMI_MAP = [
|
||||
]
|
||||
|
||||
HM_CONTROL_MODE = 'CONTROL_MODE'
|
||||
HM_IP_CONTROL_MODE = 'SET_POINT_MODE'
|
||||
HMIP_CONTROL_MODE = 'SET_POINT_MODE'
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
||||
|
||||
@@ -78,21 +77,17 @@ class HMThermostat(HMDevice, ClimateDevice):
|
||||
if HM_CONTROL_MODE not in self._data:
|
||||
return None
|
||||
|
||||
set_point_mode = self._data.get('SET_POINT_MODE', -1)
|
||||
control_mode = self._data.get('CONTROL_MODE', -1)
|
||||
boost_mode = self._data.get('BOOST_MODE', False)
|
||||
|
||||
# boost mode is active
|
||||
if boost_mode:
|
||||
if self._data.get('BOOST_MODE', False):
|
||||
return STATE_BOOST
|
||||
|
||||
# HM ip etrv 2 uses the set_point_mode to say if its
|
||||
# HmIP uses the set_point_mode to say if its
|
||||
# auto or manual
|
||||
if not set_point_mode == -1:
|
||||
code = set_point_mode
|
||||
if HMIP_CONTROL_MODE in self._data:
|
||||
code = self._data[HMIP_CONTROL_MODE]
|
||||
# Other devices use the control_mode
|
||||
else:
|
||||
code = control_mode
|
||||
code = self._data['CONTROL_MODE']
|
||||
|
||||
# get the name of the mode
|
||||
name = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][code]
|
||||
@@ -101,12 +96,15 @@ class HMThermostat(HMDevice, ClimateDevice):
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
op_list = []
|
||||
# HMIP use set_point_mode for operation
|
||||
if HMIP_CONTROL_MODE in self._data:
|
||||
return [STATE_MANUAL, STATE_AUTO, STATE_BOOST]
|
||||
|
||||
# HM
|
||||
op_list = []
|
||||
for mode in self._hmdevice.ACTIONNODE:
|
||||
if mode in HM_STATE_MAP:
|
||||
op_list.append(HM_STATE_MAP.get(mode))
|
||||
|
||||
return op_list
|
||||
|
||||
@property
|
||||
@@ -157,11 +155,11 @@ class HMThermostat(HMDevice, ClimateDevice):
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from the Homematic metadata."""
|
||||
self._state = next(iter(self._hmdevice.WRITENODE.keys()))
|
||||
self._data[self._state] = STATE_UNKNOWN
|
||||
self._data[self._state] = None
|
||||
|
||||
if HM_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE or \
|
||||
HM_IP_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE:
|
||||
self._data[HM_CONTROL_MODE] = STATE_UNKNOWN
|
||||
HMIP_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE:
|
||||
self._data[HM_CONTROL_MODE] = None
|
||||
|
||||
for node in self._hmdevice.SENSORNODE.keys():
|
||||
self._data[node] = STATE_UNKNOWN
|
||||
self._data[node] = None
|
||||
|
||||
@@ -34,10 +34,11 @@ FAN_MODES = [
|
||||
]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Iterate through and add all Melissa devices."""
|
||||
api = hass.data[DATA_MELISSA]
|
||||
devices = api.fetch_devices().values()
|
||||
devices = (await api.async_fetch_devices()).values()
|
||||
|
||||
all_devices = []
|
||||
|
||||
@@ -46,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
all_devices.append(MelissaClimate(
|
||||
api, device['serial_number'], device))
|
||||
|
||||
add_entities(all_devices)
|
||||
async_add_entities(all_devices)
|
||||
|
||||
|
||||
class MelissaClimate(ClimateDevice):
|
||||
@@ -87,6 +88,12 @@ class MelissaClimate(ClimateDevice):
|
||||
if self._data:
|
||||
return self._data[self._api.TEMP]
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity value."""
|
||||
if self._data:
|
||||
return self._data[self._api.HUMIDITY]
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
@@ -112,8 +119,9 @@ class MelissaClimate(ClimateDevice):
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._cur_settings is not None:
|
||||
return self._cur_settings[self._api.TEMP]
|
||||
if self._cur_settings is None:
|
||||
return None
|
||||
return self._cur_settings[self._api.TEMP]
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@@ -142,48 +150,48 @@ class MelissaClimate(ClimateDevice):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
self.send({self._api.TEMP: temp})
|
||||
await self.async_send({self._api.TEMP: temp})
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set fan mode."""
|
||||
melissa_fan_mode = self.hass_fan_to_melissa(fan_mode)
|
||||
self.send({self._api.FAN: melissa_fan_mode})
|
||||
await self.async_send({self._api.FAN: melissa_fan_mode})
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
async def async_set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
mode = self.hass_mode_to_melissa(operation_mode)
|
||||
self.send({self._api.MODE: mode})
|
||||
await self.async_send({self._api.MODE: mode})
|
||||
|
||||
def turn_on(self):
|
||||
async def async_turn_on(self):
|
||||
"""Turn on device."""
|
||||
self.send({self._api.STATE: self._api.STATE_ON})
|
||||
await self.async_send({self._api.STATE: self._api.STATE_ON})
|
||||
|
||||
def turn_off(self):
|
||||
async def async_turn_off(self):
|
||||
"""Turn off device."""
|
||||
self.send({self._api.STATE: self._api.STATE_OFF})
|
||||
await self.async_send({self._api.STATE: self._api.STATE_OFF})
|
||||
|
||||
def send(self, value):
|
||||
async def async_send(self, value):
|
||||
"""Send action to service."""
|
||||
try:
|
||||
old_value = self._cur_settings.copy()
|
||||
self._cur_settings.update(value)
|
||||
except AttributeError:
|
||||
old_value = None
|
||||
if not self._api.send(self._serial_number, self._cur_settings):
|
||||
if not await self._api.async_send(
|
||||
self._serial_number, self._cur_settings):
|
||||
self._cur_settings = old_value
|
||||
return False
|
||||
return True
|
||||
|
||||
def update(self):
|
||||
async def async_update(self):
|
||||
"""Get latest data from Melissa."""
|
||||
try:
|
||||
self._data = self._api.status(cached=True)[self._serial_number]
|
||||
self._cur_settings = self._api.cur_settings(
|
||||
self._data = (await self._api.async_status(cached=True))[
|
||||
self._serial_number]
|
||||
self._cur_settings = (await self._api.async_cur_settings(
|
||||
self._serial_number
|
||||
)['controller']['_relation']['command_log']
|
||||
))['controller']['_relation']['command_log']
|
||||
except KeyError:
|
||||
_LOGGER.warning(
|
||||
'Unable to update entity %s', self.entity_id)
|
||||
|
||||
@@ -8,29 +8,44 @@ https://home-assistant.io/components/climate.mill/
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_FAN_MODE, SUPPORT_ON_OFF)
|
||||
ClimateDevice, DOMAIN, PLATFORM_SCHEMA, STATE_HEAT,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE,
|
||||
SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_PASSWORD, CONF_USERNAME,
|
||||
STATE_ON, STATE_OFF, TEMP_CELSIUS)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
REQUIREMENTS = ['millheater==0.1.2']
|
||||
REQUIREMENTS = ['millheater==0.2.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_AWAY_TEMP = 'away_temp'
|
||||
ATTR_COMFORT_TEMP = 'comfort_temp'
|
||||
ATTR_ROOM_NAME = 'room_name'
|
||||
ATTR_SLEEP_TEMP = 'sleep_temp'
|
||||
MAX_TEMP = 35
|
||||
MIN_TEMP = 5
|
||||
SERVICE_SET_ROOM_TEMP = 'mill_set_room_temperature'
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
|
||||
SUPPORT_FAN_MODE | SUPPORT_ON_OFF)
|
||||
SUPPORT_FAN_MODE)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
})
|
||||
|
||||
SET_ROOM_TEMP_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ROOM_NAME): cv.string,
|
||||
vol.Optional(ATTR_AWAY_TEMP): cv.positive_int,
|
||||
vol.Optional(ATTR_COMFORT_TEMP): cv.positive_int,
|
||||
vol.Optional(ATTR_SLEEP_TEMP): cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
@@ -43,13 +58,27 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
_LOGGER.error("Failed to connect to Mill")
|
||||
return
|
||||
|
||||
await mill_data_connection.update_heaters()
|
||||
await mill_data_connection.find_all_heaters()
|
||||
|
||||
dev = []
|
||||
for heater in mill_data_connection.heaters.values():
|
||||
dev.append(MillHeater(heater, mill_data_connection))
|
||||
async_add_entities(dev)
|
||||
|
||||
async def set_room_temp(service):
|
||||
"""Set room temp."""
|
||||
room_name = service.data.get(ATTR_ROOM_NAME)
|
||||
sleep_temp = service.data.get(ATTR_SLEEP_TEMP)
|
||||
comfort_temp = service.data.get(ATTR_COMFORT_TEMP)
|
||||
away_temp = service.data.get(ATTR_AWAY_TEMP)
|
||||
await mill_data_connection.set_room_temperatures_by_name(room_name,
|
||||
sleep_temp,
|
||||
comfort_temp,
|
||||
away_temp)
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_SET_ROOM_TEMP,
|
||||
set_room_temp, schema=SET_ROOM_TEMP_SCHEMA)
|
||||
|
||||
|
||||
class MillHeater(ClimateDevice):
|
||||
"""Representation of a Mill Thermostat device."""
|
||||
@@ -62,12 +91,14 @@ class MillHeater(ClimateDevice):
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
if self._heater.is_gen1:
|
||||
return SUPPORT_FLAGS
|
||||
return SUPPORT_FLAGS | SUPPORT_ON_OFF | SUPPORT_OPERATION_MODE
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._heater.device_status == 0 # weird api choice
|
||||
return self._heater.available
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
@@ -79,6 +110,22 @@ class MillHeater(ClimateDevice):
|
||||
"""Return the name of the entity."""
|
||||
return self._heater.name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
res = {
|
||||
"open_window": self._heater.open_window,
|
||||
"heating": self._heater.is_heating,
|
||||
"controlled_by_tibber": self._heater.tibber_control,
|
||||
"heater_generation": 1 if self._heater.is_gen1 else 2,
|
||||
}
|
||||
if self._heater.room:
|
||||
res['room'] = self._heater.room.name
|
||||
res['avg_room_temp'] = self._heater.room.avg_temp
|
||||
else:
|
||||
res['room'] = "Independent device"
|
||||
return res
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement which this thermostat uses."""
|
||||
@@ -112,6 +159,8 @@ class MillHeater(ClimateDevice):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if heater is on."""
|
||||
if self._heater.is_gen1:
|
||||
return True
|
||||
return self._heater.power_status == 1
|
||||
|
||||
@property
|
||||
@@ -124,6 +173,18 @@ class MillHeater(ClimateDevice):
|
||||
"""Return the maximum temperature."""
|
||||
return MAX_TEMP
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation."""
|
||||
return STATE_HEAT if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
if self._heater.is_gen1:
|
||||
return None
|
||||
return [STATE_HEAT, STATE_OFF]
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
@@ -151,3 +212,12 @@ class MillHeater(ClimateDevice):
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._heater = await self._conn.update_device(self._heater.device_id)
|
||||
|
||||
async def async_set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_HEAT:
|
||||
await self.async_turn_on()
|
||||
elif operation_mode == STATE_OFF and not self._heater.is_gen1:
|
||||
await self.async_turn_off()
|
||||
else:
|
||||
_LOGGER.error("Unrecognized operation mode: %s", operation_mode)
|
||||
|
||||
@@ -168,18 +168,14 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._mode != NEST_MODE_HEAT_COOL and \
|
||||
self._mode != STATE_ECO and \
|
||||
not self.is_away_mode_on:
|
||||
if self._mode not in (NEST_MODE_HEAT_COOL, STATE_ECO):
|
||||
return self._target_temperature
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
if (self.is_away_mode_on or self._mode == STATE_ECO) and \
|
||||
self._eco_temperature[0]:
|
||||
# eco_temperature is always a low, high tuple
|
||||
if self._mode == STATE_ECO:
|
||||
return self._eco_temperature[0]
|
||||
if self._mode == NEST_MODE_HEAT_COOL:
|
||||
return self._target_temperature[0]
|
||||
@@ -188,9 +184,7 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the upper bound temperature we try to reach."""
|
||||
if (self.is_away_mode_on or self._mode == STATE_ECO) and \
|
||||
self._eco_temperature[1]:
|
||||
# eco_temperature is always a low, high tuple
|
||||
if self._mode == STATE_ECO:
|
||||
return self._eco_temperature[1]
|
||||
if self._mode == NEST_MODE_HEAT_COOL:
|
||||
return self._target_temperature[1]
|
||||
|
||||
@@ -116,6 +116,22 @@ ecobee_resume_program:
|
||||
description: Resume all events and return to the scheduled program. This default to false which removes only the top event.
|
||||
example: true
|
||||
|
||||
mill_set_room_temperature:
|
||||
description: Set Mill room temperatures.
|
||||
fields:
|
||||
room_name:
|
||||
description: Name of room to change.
|
||||
example: 'kitchen'
|
||||
away_temp:
|
||||
description: Away temp.
|
||||
example: 12
|
||||
comfort_temp:
|
||||
description: Comfort temp.
|
||||
example: 22
|
||||
sleep_temp:
|
||||
description: Sleep temp.
|
||||
example: 17
|
||||
|
||||
nuheat_resume_program:
|
||||
description: Resume the programmed schedule.
|
||||
fields:
|
||||
|
||||
74
homeassistant/components/climate/velbus.py
Normal file
74
homeassistant/components/climate/velbus.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Support for Velbus thermostat.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/climate.velbus/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, SUPPORT_TARGET_TEMPERATURE, ClimateDevice)
|
||||
from homeassistant.components.velbus import (
|
||||
DOMAIN as VELBUS_DOMAIN, VelbusEntity)
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['velbus']
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the Velbus thermostat platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
sensors = []
|
||||
for sensor in discovery_info:
|
||||
module = hass.data[VELBUS_DOMAIN].get_module(sensor[0])
|
||||
channel = sensor[1]
|
||||
sensors.append(VelbusClimate(module, channel))
|
||||
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
class VelbusClimate(VelbusEntity, ClimateDevice):
|
||||
"""Representation of a Velbus thermostat."""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list off supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit this state is expressed in."""
|
||||
if self._module.get_unit(self._channel) == '°C':
|
||||
return TEMP_CELSIUS
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._module.get_state(self._channel)
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation."""
|
||||
return STATE_HEAT
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._module.get_climate_target()
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperatures."""
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temp is None:
|
||||
return
|
||||
self._module.set_temp(temp)
|
||||
self.schedule_update_ha_state()
|
||||
@@ -8,9 +8,12 @@ import logging
|
||||
|
||||
from homeassistant.util import convert
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, ENTITY_ID_FORMAT, SUPPORT_TARGET_TEMPERATURE,
|
||||
ClimateDevice, STATE_AUTO, STATE_COOL,
|
||||
STATE_HEAT, ENTITY_ID_FORMAT, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE)
|
||||
from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
TEMP_FAHRENHEIT,
|
||||
TEMP_CELSIUS,
|
||||
ATTR_TEMPERATURE)
|
||||
@@ -22,8 +25,8 @@ DEPENDENCIES = ['vera']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
OPERATION_LIST = ['Heat', 'Cool', 'Auto Changeover', 'Off']
|
||||
FAN_OPERATION_LIST = ['On', 'Auto', 'Cycle']
|
||||
OPERATION_LIST = [STATE_HEAT, STATE_COOL, STATE_AUTO, STATE_OFF]
|
||||
FAN_OPERATION_LIST = [STATE_ON, STATE_AUTO]
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_FAN_MODE)
|
||||
@@ -54,13 +57,13 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
mode = self.vera_device.get_hvac_mode()
|
||||
if mode == 'HeatOn':
|
||||
return OPERATION_LIST[0] # heat
|
||||
return OPERATION_LIST[0] # Heat
|
||||
if mode == 'CoolOn':
|
||||
return OPERATION_LIST[1] # cool
|
||||
return OPERATION_LIST[1] # Cool
|
||||
if mode == 'AutoChangeOver':
|
||||
return OPERATION_LIST[2] # auto
|
||||
return OPERATION_LIST[2] # Auto
|
||||
if mode == 'Off':
|
||||
return OPERATION_LIST[3] # off
|
||||
return OPERATION_LIST[3] # Off
|
||||
return 'Off'
|
||||
|
||||
@property
|
||||
@@ -76,8 +79,6 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
return FAN_OPERATION_LIST[0] # on
|
||||
if mode == "Auto":
|
||||
return FAN_OPERATION_LIST[1] # auto
|
||||
if mode == "PeriodicOn":
|
||||
return FAN_OPERATION_LIST[2] # cycle
|
||||
return "Auto"
|
||||
|
||||
@property
|
||||
@@ -89,10 +90,8 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
"""Set new target temperature."""
|
||||
if fan_mode == FAN_OPERATION_LIST[0]:
|
||||
self.vera_device.fan_on()
|
||||
elif fan_mode == FAN_OPERATION_LIST[1]:
|
||||
else:
|
||||
self.vera_device.fan_auto()
|
||||
elif fan_mode == FAN_OPERATION_LIST[2]:
|
||||
return self.vera_device.fan_cycle()
|
||||
|
||||
@property
|
||||
def current_power_w(self):
|
||||
|
||||
@@ -12,24 +12,20 @@ import os
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME)
|
||||
EVENT_HOMEASSISTANT_START, CLOUD_NEVER_EXPOSED_ENTITIES, CONF_REGION,
|
||||
CONF_MODE, CONF_NAME)
|
||||
from homeassistant.helpers import entityfilter, config_validation as cv
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.components.alexa import smart_home as alexa_sh
|
||||
from homeassistant.components.google_assistant import helpers as ga_h
|
||||
from homeassistant.components.google_assistant import const as ga_c
|
||||
|
||||
from . import http_api, iot, auth_api
|
||||
from . import http_api, iot, auth_api, prefs
|
||||
from .const import CONFIG_DIR, DOMAIN, SERVERS
|
||||
|
||||
REQUIREMENTS = ['warrant==0.6.1']
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_ENABLE_ALEXA = 'alexa_enabled'
|
||||
STORAGE_ENABLE_GOOGLE = 'google_enabled'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_UNDEF = object()
|
||||
|
||||
CONF_ALEXA = 'alexa'
|
||||
CONF_ALIASES = 'aliases'
|
||||
@@ -68,7 +64,7 @@ ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend({
|
||||
})
|
||||
|
||||
GACTIONS_SCHEMA = ASSISTANT_SCHEMA.extend({
|
||||
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: GOOGLE_ENTITY_SCHEMA}
|
||||
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: GOOGLE_ENTITY_SCHEMA},
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
@@ -122,14 +118,13 @@ class Cloud:
|
||||
self.hass = hass
|
||||
self.mode = mode
|
||||
self.alexa_config = alexa
|
||||
self._google_actions = google_actions
|
||||
self.google_actions_user_conf = google_actions
|
||||
self._gactions_config = None
|
||||
self._prefs = None
|
||||
self.prefs = prefs.CloudPreferences(hass)
|
||||
self.id_token = None
|
||||
self.access_token = None
|
||||
self.refresh_token = None
|
||||
self.iot = iot.CloudIoT(self)
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
if mode == MODE_DEV:
|
||||
self.cognito_client_id = cognito_client_id
|
||||
@@ -180,30 +175,24 @@ class Cloud:
|
||||
def gactions_config(self):
|
||||
"""Return the Google Assistant config."""
|
||||
if self._gactions_config is None:
|
||||
conf = self._google_actions
|
||||
conf = self.google_actions_user_conf
|
||||
|
||||
def should_expose(entity):
|
||||
"""If an entity should be exposed."""
|
||||
if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
return False
|
||||
|
||||
return conf['filter'](entity.entity_id)
|
||||
|
||||
self._gactions_config = ga_h.Config(
|
||||
should_expose=should_expose,
|
||||
agent_user_id=self.claims['cognito:username'],
|
||||
entity_config=conf.get(CONF_ENTITY_CONFIG),
|
||||
allow_unlock=self.prefs.google_allow_unlock,
|
||||
)
|
||||
|
||||
return self._gactions_config
|
||||
|
||||
@property
|
||||
def alexa_enabled(self):
|
||||
"""Return if Alexa is enabled."""
|
||||
return self._prefs[STORAGE_ENABLE_ALEXA]
|
||||
|
||||
@property
|
||||
def google_enabled(self):
|
||||
"""Return if Google is enabled."""
|
||||
return self._prefs[STORAGE_ENABLE_GOOGLE]
|
||||
|
||||
def path(self, *parts):
|
||||
"""Get config path inside cloud dir.
|
||||
|
||||
@@ -243,20 +232,6 @@ class Cloud:
|
||||
|
||||
async def async_start(self, _):
|
||||
"""Start the cloud component."""
|
||||
prefs = await self._store.async_load()
|
||||
if prefs is None:
|
||||
prefs = {}
|
||||
if self.mode not in prefs:
|
||||
# Default to True if already logged in to make this not a
|
||||
# breaking change.
|
||||
enabled = await self.hass.async_add_executor_job(
|
||||
os.path.isfile, self.user_info_path)
|
||||
prefs = {
|
||||
STORAGE_ENABLE_ALEXA: enabled,
|
||||
STORAGE_ENABLE_GOOGLE: enabled,
|
||||
}
|
||||
self._prefs = prefs
|
||||
|
||||
def load_config():
|
||||
"""Load config."""
|
||||
# Ensure config dir exists
|
||||
@@ -273,6 +248,8 @@ class Cloud:
|
||||
|
||||
info = await self.hass.async_add_job(load_config)
|
||||
|
||||
await self.prefs.async_initialize(not info)
|
||||
|
||||
if info is None:
|
||||
return
|
||||
|
||||
@@ -282,15 +259,6 @@ class Cloud:
|
||||
|
||||
self.hass.add_job(self.iot.connect())
|
||||
|
||||
async def update_preferences(self, *, google_enabled=_UNDEF,
|
||||
alexa_enabled=_UNDEF):
|
||||
"""Update user preferences."""
|
||||
if google_enabled is not _UNDEF:
|
||||
self._prefs[STORAGE_ENABLE_GOOGLE] = google_enabled
|
||||
if alexa_enabled is not _UNDEF:
|
||||
self._prefs[STORAGE_ENABLE_ALEXA] = alexa_enabled
|
||||
await self._store.async_save(self._prefs)
|
||||
|
||||
def _decode_claims(self, token): # pylint: disable=no-self-use
|
||||
"""Decode the claims in a token."""
|
||||
from jose import jwt
|
||||
|
||||
@@ -144,7 +144,7 @@ def _authenticate(cloud, email, password):
|
||||
cognito.authenticate(password=password)
|
||||
return cognito
|
||||
|
||||
except ForceChangePasswordException as err:
|
||||
except ForceChangePasswordException:
|
||||
raise PasswordChangeRequired
|
||||
|
||||
except ClientError as err:
|
||||
|
||||
@@ -3,6 +3,10 @@ DOMAIN = 'cloud'
|
||||
CONFIG_DIR = '.cloud'
|
||||
REQUEST_TIMEOUT = 10
|
||||
|
||||
PREF_ENABLE_ALEXA = 'alexa_enabled'
|
||||
PREF_ENABLE_GOOGLE = 'google_enabled'
|
||||
PREF_GOOGLE_ALLOW_UNLOCK = 'google_allow_unlock'
|
||||
|
||||
SERVERS = {
|
||||
'production': {
|
||||
'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u',
|
||||
|
||||
@@ -11,9 +11,13 @@ from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http.data_validator import (
|
||||
RequestDataValidator)
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.alexa import smart_home as alexa_sh
|
||||
from homeassistant.components.google_assistant import smart_home as google_sh
|
||||
|
||||
from . import auth_api
|
||||
from .const import DOMAIN, REQUEST_TIMEOUT
|
||||
from .const import (
|
||||
DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
|
||||
PREF_GOOGLE_ALLOW_UNLOCK)
|
||||
from .iot import STATE_DISCONNECTED, STATE_CONNECTED
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -28,8 +32,9 @@ SCHEMA_WS_STATUS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
WS_TYPE_UPDATE_PREFS = 'cloud/update_prefs'
|
||||
SCHEMA_WS_UPDATE_PREFS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_UPDATE_PREFS,
|
||||
vol.Optional('google_enabled'): bool,
|
||||
vol.Optional('alexa_enabled'): bool,
|
||||
vol.Optional(PREF_ENABLE_GOOGLE): bool,
|
||||
vol.Optional(PREF_ENABLE_ALEXA): bool,
|
||||
vol.Optional(PREF_GOOGLE_ALLOW_UNLOCK): bool,
|
||||
})
|
||||
|
||||
|
||||
@@ -286,7 +291,7 @@ async def websocket_update_prefs(hass, connection, msg):
|
||||
changes = dict(msg)
|
||||
changes.pop('id')
|
||||
changes.pop('type')
|
||||
await cloud.update_preferences(**changes)
|
||||
await cloud.prefs.async_update(**changes)
|
||||
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg['id'], {'success': True}))
|
||||
@@ -306,6 +311,9 @@ def _account_data(cloud):
|
||||
'logged_in': True,
|
||||
'email': claims['email'],
|
||||
'cloud': cloud.iot.state,
|
||||
'google_enabled': cloud.google_enabled,
|
||||
'alexa_enabled': cloud.alexa_enabled,
|
||||
'prefs': cloud.prefs.as_dict(),
|
||||
'google_entities': cloud.google_actions_user_conf['filter'].config,
|
||||
'google_domains': list(google_sh.DOMAIN_TO_GOOGLE_TYPES),
|
||||
'alexa_entities': cloud.alexa_config.should_expose.config,
|
||||
'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS),
|
||||
}
|
||||
|
||||
@@ -227,11 +227,9 @@ def async_handle_message(hass, cloud, handler_name, payload):
|
||||
@asyncio.coroutine
|
||||
def async_handle_alexa(hass, cloud, payload):
|
||||
"""Handle an incoming IoT message for Alexa."""
|
||||
if not cloud.alexa_enabled:
|
||||
return alexa.turned_off_response(payload)
|
||||
|
||||
result = yield from alexa.async_handle_message(
|
||||
hass, cloud.alexa_config, payload)
|
||||
hass, cloud.alexa_config, payload,
|
||||
enabled=cloud.prefs.alexa_enabled)
|
||||
return result
|
||||
|
||||
|
||||
@@ -239,7 +237,7 @@ def async_handle_alexa(hass, cloud, payload):
|
||||
@asyncio.coroutine
|
||||
def async_handle_google_actions(hass, cloud, payload):
|
||||
"""Handle an incoming IoT message for Google Actions."""
|
||||
if not cloud.google_enabled:
|
||||
if not cloud.prefs.google_enabled:
|
||||
return ga.turned_off_response(payload)
|
||||
|
||||
result = yield from ga.async_handle_message(
|
||||
|
||||
63
homeassistant/components/cloud/prefs.py
Normal file
63
homeassistant/components/cloud/prefs.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Preference management for cloud."""
|
||||
from .const import (
|
||||
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
|
||||
PREF_GOOGLE_ALLOW_UNLOCK)
|
||||
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION = 1
|
||||
_UNDEF = object()
|
||||
|
||||
|
||||
class CloudPreferences:
|
||||
"""Handle cloud preferences."""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize cloud prefs."""
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
self._prefs = None
|
||||
|
||||
async def async_initialize(self, logged_in):
|
||||
"""Finish initializing the preferences."""
|
||||
prefs = await self._store.async_load()
|
||||
|
||||
if prefs is None:
|
||||
# Backwards compat: we enable alexa/google if already logged in
|
||||
prefs = {
|
||||
PREF_ENABLE_ALEXA: logged_in,
|
||||
PREF_ENABLE_GOOGLE: logged_in,
|
||||
PREF_GOOGLE_ALLOW_UNLOCK: False,
|
||||
}
|
||||
|
||||
self._prefs = prefs
|
||||
|
||||
async def async_update(self, *, google_enabled=_UNDEF,
|
||||
alexa_enabled=_UNDEF, google_allow_unlock=_UNDEF):
|
||||
"""Update user preferences."""
|
||||
for key, value in (
|
||||
(PREF_ENABLE_GOOGLE, google_enabled),
|
||||
(PREF_ENABLE_ALEXA, alexa_enabled),
|
||||
(PREF_GOOGLE_ALLOW_UNLOCK, google_allow_unlock),
|
||||
):
|
||||
if value is not _UNDEF:
|
||||
self._prefs[key] = value
|
||||
|
||||
await self._store.async_save(self._prefs)
|
||||
|
||||
def as_dict(self):
|
||||
"""Return dictionary version."""
|
||||
return self._prefs
|
||||
|
||||
@property
|
||||
def alexa_enabled(self):
|
||||
"""Return if Alexa is enabled."""
|
||||
return self._prefs[PREF_ENABLE_ALEXA]
|
||||
|
||||
@property
|
||||
def google_enabled(self):
|
||||
"""Return if Google is enabled."""
|
||||
return self._prefs[PREF_ENABLE_GOOGLE]
|
||||
|
||||
@property
|
||||
def google_allow_unlock(self):
|
||||
"""Return if Google is allowed to unlock locks."""
|
||||
return self._prefs.get(PREF_GOOGLE_ALLOW_UNLOCK, False)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user