forked from home-assistant/core
Compare commits
438 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbb4c43353 | ||
|
|
59891fa838 | ||
|
|
475ab68853 | ||
|
|
d3f8ad15a4 | ||
|
|
c45fc84859 | ||
|
|
2a09ac017f | ||
|
|
f9e8d4237d | ||
|
|
30e16c97fc | ||
|
|
0e1f664102 | ||
|
|
60ca79ce35 | ||
|
|
f576b37e9f | ||
|
|
592f9901f9 | ||
|
|
7991e2df5f | ||
|
|
5876d6766d | ||
|
|
e13fd05e7d | ||
|
|
a760673ad6 | ||
|
|
12dec93565 | ||
|
|
c376bc2e45 | ||
|
|
f0e5f68865 | ||
|
|
56f4486e0b | ||
|
|
828c469ef7 | ||
|
|
0a6d519b9d | ||
|
|
0c97fe7eac | ||
|
|
e8ce41874c | ||
|
|
0ab0e35d59 | ||
|
|
51108b8fe9 | ||
|
|
9e6817b6d0 | ||
|
|
74581b57f8 | ||
|
|
4fcaea23a8 | ||
|
|
b59c29943b | ||
|
|
1e8c00ac02 | ||
|
|
9d5c61b2f0 | ||
|
|
f5eeb252a7 | ||
|
|
3b4ea864a1 | ||
|
|
3318f02664 | ||
|
|
438edc5ca1 | ||
|
|
abcfcdd887 | ||
|
|
fff269e790 | ||
|
|
81a27e726c | ||
|
|
7c120748ce | ||
|
|
e83816c055 | ||
|
|
cd2703e121 | ||
|
|
c2828bac2c | ||
|
|
ad7370e1c2 | ||
|
|
3b7f16f189 | ||
|
|
cc03f7ee6a | ||
|
|
ecc1429453 | ||
|
|
98568b5eb7 | ||
|
|
9d9ca64f26 | ||
|
|
1d31137616 | ||
|
|
f86bd15580 | ||
|
|
cbf65220aa | ||
|
|
c100b8cb52 | ||
|
|
654ad41464 | ||
|
|
a2abb4ae0a | ||
|
|
36e266442f | ||
|
|
f3d9086ff4 | ||
|
|
0c09cfc6c4 | ||
|
|
b0b6026c68 | ||
|
|
8f47a9109c | ||
|
|
f0293eeac2 | ||
|
|
e4317a6741 | ||
|
|
4b449f5f93 | ||
|
|
8760dc9b29 | ||
|
|
1831a7da68 | ||
|
|
3e34f34f6b | ||
|
|
3fec2955a5 | ||
|
|
2cf9254a08 | ||
|
|
333da0dc6d | ||
|
|
7b10f0a14f | ||
|
|
fb6bdfaba9 | ||
|
|
d7da90ae54 | ||
|
|
a5bfcceacd | ||
|
|
4961ece931 | ||
|
|
7d99d6aad9 | ||
|
|
6dc93c2751 | ||
|
|
5c39eebea8 | ||
|
|
ffd295b38b | ||
|
|
5d810dae86 | ||
|
|
486bcc4cae | ||
|
|
cc2de5e1dc | ||
|
|
77d8e393a1 | ||
|
|
c6bf529d38 | ||
|
|
dac9716cf4 | ||
|
|
9043895407 | ||
|
|
2f08a91fdd | ||
|
|
1807b45222 | ||
|
|
b4f392b181 | ||
|
|
8e8ec7a7c3 | ||
|
|
7edf14e55f | ||
|
|
7bea69ce83 | ||
|
|
8d31c5fbf6 | ||
|
|
dc42b6358a | ||
|
|
06ceadfd54 | ||
|
|
4359e0babf | ||
|
|
ee153062ab | ||
|
|
fada6d3f49 | ||
|
|
f6a5e0887d | ||
|
|
4f8d2ec317 | ||
|
|
e63a96cf53 | ||
|
|
a5c0831dc1 | ||
|
|
718949481f | ||
|
|
90639d33ab | ||
|
|
966809c1a1 | ||
|
|
bc27d173d0 | ||
|
|
fde291f866 | ||
|
|
49c399c358 | ||
|
|
8d1999dc12 | ||
|
|
ee05a4ab89 | ||
|
|
8a42e1551a | ||
|
|
9cc3e7e47b | ||
|
|
54755df9ea | ||
|
|
84ebcd8a59 | ||
|
|
f1280d3edb | ||
|
|
c27074e6f7 | ||
|
|
c8bfcd2ed4 | ||
|
|
42699b7a60 | ||
|
|
6bc07298d3 | ||
|
|
4ece4bf241 | ||
|
|
1a86fa5a02 | ||
|
|
d54a634f11 | ||
|
|
5e1ff20b09 | ||
|
|
29266213a0 | ||
|
|
2aa89cfe07 | ||
|
|
879c816f5c | ||
|
|
4ae11c009d | ||
|
|
dcd6f7a29e | ||
|
|
fde4a7d029 | ||
|
|
b83ff739bc | ||
|
|
8c9b3898fc | ||
|
|
95e0027924 | ||
|
|
c67c20f752 | ||
|
|
1a1571cd52 | ||
|
|
cca0d3ed44 | ||
|
|
f0479855bd | ||
|
|
40aafcdf5d | ||
|
|
8c9557401f | ||
|
|
ffd3081743 | ||
|
|
d0275c8075 | ||
|
|
f6c3832e90 | ||
|
|
d29bdddaa7 | ||
|
|
d3be056d15 | ||
|
|
bffa0d2b04 | ||
|
|
23b65bfb30 | ||
|
|
1d4a7f1160 | ||
|
|
dc08852fc2 | ||
|
|
3377f30613 | ||
|
|
84ca4d2a21 | ||
|
|
1366c93c83 | ||
|
|
e5e2a151aa | ||
|
|
bd1e533409 | ||
|
|
21e82bd037 | ||
|
|
af9a0e8fea | ||
|
|
abc5c3e128 | ||
|
|
543e8bb62e | ||
|
|
6ca828fd14 | ||
|
|
87b83f3602 | ||
|
|
5829cdfdf1 | ||
|
|
d473f3407b | ||
|
|
9373d5e901 | ||
|
|
d8abef9210 | ||
|
|
4fde0ffe9c | ||
|
|
ba019c799a | ||
|
|
5581c6295e | ||
|
|
192db5bec3 | ||
|
|
b8eaec565a | ||
|
|
e0f35c0279 | ||
|
|
2eeeb9075a | ||
|
|
71ee290bfd | ||
|
|
7aad93e90d | ||
|
|
a65f22378e | ||
|
|
bb9db28c95 | ||
|
|
d10f017441 | ||
|
|
b6e0286d71 | ||
|
|
4451d2e847 | ||
|
|
229000b834 | ||
|
|
9704057959 | ||
|
|
effb9e9d23 | ||
|
|
effbb3bd4c | ||
|
|
471501d386 | ||
|
|
ef94b5c77a | ||
|
|
60dcc9a5c0 | ||
|
|
5b4862cc3c | ||
|
|
fbf945c18b | ||
|
|
609c25691a | ||
|
|
6e77877743 | ||
|
|
7b105a2150 | ||
|
|
ee57a823af | ||
|
|
04b1621b65 | ||
|
|
f5e24cb0bb | ||
|
|
ac72dea09a | ||
|
|
2f474a0ed8 | ||
|
|
7a4cc8e082 | ||
|
|
92dc76773a | ||
|
|
fe4abc8454 | ||
|
|
821d01f82c | ||
|
|
b453834b2f | ||
|
|
97f14015ea | ||
|
|
4fb25cf16d | ||
|
|
e7b5c5812c | ||
|
|
2ac423bd9d | ||
|
|
ec7ca9a560 | ||
|
|
cb298123d4 | ||
|
|
c5bf4fe339 | ||
|
|
57c5ed33ee | ||
|
|
3be0103259 | ||
|
|
614b5da170 | ||
|
|
acf6d4ab82 | ||
|
|
d3acb25070 | ||
|
|
222ad3ab6d | ||
|
|
5ae2bcdbb7 | ||
|
|
6c9742afc4 | ||
|
|
cf924cd14d | ||
|
|
f2267437df | ||
|
|
233920f22c | ||
|
|
7536e825fa | ||
|
|
e12a9eaadd | ||
|
|
fb184b4b6f | ||
|
|
63ff173305 | ||
|
|
903e6b5aee | ||
|
|
46ce26eb7a | ||
|
|
b1bba3675d | ||
|
|
ed5d10448e | ||
|
|
652c006cbc | ||
|
|
b67c5df525 | ||
|
|
a7d5a8d93e | ||
|
|
c48c2b00a8 | ||
|
|
9bc5cd2d4b | ||
|
|
ecf3a9cb36 | ||
|
|
074e31bcf9 | ||
|
|
63cc658010 | ||
|
|
8682f21fc5 | ||
|
|
aa28e6727d | ||
|
|
12129f0e6a | ||
|
|
8a7cfce67b | ||
|
|
5e71e9b826 | ||
|
|
db8bb53984 | ||
|
|
692f4c293b | ||
|
|
da37380410 | ||
|
|
fa4aa2244e | ||
|
|
c63bdd5afe | ||
|
|
20a9899354 | ||
|
|
fe6a4b8ae5 | ||
|
|
143044f8f1 | ||
|
|
d655c0e358 | ||
|
|
46e030662d | ||
|
|
5779d64e98 | ||
|
|
83a5f932d1 | ||
|
|
a12fa2e5bf | ||
|
|
ee37fc344b | ||
|
|
8cc0748db3 | ||
|
|
0ecceb601b | ||
|
|
2a1a5e53a1 | ||
|
|
c8b782189e | ||
|
|
74016c4179 | ||
|
|
c30c8df449 | ||
|
|
58de661ad5 | ||
|
|
f4a97db783 | ||
|
|
b220ceec9c | ||
|
|
fb796b5481 | ||
|
|
ea5bec3ef4 | ||
|
|
8185587100 | ||
|
|
061a38cc3b | ||
|
|
23fc5e2c9f | ||
|
|
6496c38ce6 | ||
|
|
3363b88a73 | ||
|
|
2e17d0926a | ||
|
|
85ac50cc77 | ||
|
|
da61b18392 | ||
|
|
8a88af20da | ||
|
|
f8527e9773 | ||
|
|
7977996c0d | ||
|
|
22681fbe08 | ||
|
|
1e655eea74 | ||
|
|
8d940fb585 | ||
|
|
afe3dd8dbb | ||
|
|
bf96f28e95 | ||
|
|
5cba3085b4 | ||
|
|
407a419c83 | ||
|
|
4ab778fd97 | ||
|
|
ee7d4710c4 | ||
|
|
3a6434f566 | ||
|
|
a2f5b630d6 | ||
|
|
3f2fa0ed5a | ||
|
|
865865ca0f | ||
|
|
05ced33648 | ||
|
|
b4165fe9f3 | ||
|
|
47aa8c387a | ||
|
|
2b94857ffd | ||
|
|
7461c57542 | ||
|
|
632f9a21b6 | ||
|
|
da44f80b32 | ||
|
|
0bf5021c2c | ||
|
|
8fb49e8687 | ||
|
|
b82003ae08 | ||
|
|
5f8dc8af20 | ||
|
|
c13fdd23c1 | ||
|
|
e6e0e5263a | ||
|
|
0981956caa | ||
|
|
d267fc608f | ||
|
|
e077998d38 | ||
|
|
d3bc8519c0 | ||
|
|
d3adc6ddfb | ||
|
|
a3f586d097 | ||
|
|
f8c7fd212f | ||
|
|
b1f3492fd0 | ||
|
|
7123ec14be | ||
|
|
8e4394f173 | ||
|
|
5e56bc7464 | ||
|
|
ed20f7e359 | ||
|
|
74acc5cf41 | ||
|
|
0bcb7839fb | ||
|
|
17237e9d3f | ||
|
|
a663dbada0 | ||
|
|
96e1d5524a | ||
|
|
33fd2250fd | ||
|
|
31f17a91e6 | ||
|
|
d0720ac699 | ||
|
|
05acf1c10a | ||
|
|
27c92937f2 | ||
|
|
a328df6014 | ||
|
|
1fb4eefc2c | ||
|
|
0f12b4c955 | ||
|
|
a9f14b67a8 | ||
|
|
445065700c | ||
|
|
4bd96fd437 | ||
|
|
5dde0c2201 | ||
|
|
6846a76c46 | ||
|
|
fa6e93f0c7 | ||
|
|
5ef274adce | ||
|
|
e39f7d3ef5 | ||
|
|
88b9503962 | ||
|
|
596093d564 | ||
|
|
23400c4b0a | ||
|
|
af54311718 | ||
|
|
442dcd584b | ||
|
|
1e4aec63ed | ||
|
|
80c187f8ea | ||
|
|
d73b695e73 | ||
|
|
f02d169864 | ||
|
|
2dd7f0616e | ||
|
|
2f2952e0ec | ||
|
|
8358542ce0 | ||
|
|
4ca5ed25bc | ||
|
|
7bf6ceafec | ||
|
|
1cfed4f015 | ||
|
|
a082ffca1d | ||
|
|
1b563b0640 | ||
|
|
1fe189e9cb | ||
|
|
edeb92ea42 | ||
|
|
c1095665e9 | ||
|
|
2a1f8af10a | ||
|
|
6234f2d73f | ||
|
|
b488663f2c | ||
|
|
a55d8776ff | ||
|
|
5ceb4c404d | ||
|
|
0061cece0c | ||
|
|
0099168ff8 | ||
|
|
87c89752ab | ||
|
|
45f6f4443a | ||
|
|
f1290d3135 | ||
|
|
746aae51ec | ||
|
|
da9430ed12 | ||
|
|
bef22076ea | ||
|
|
fe93b51017 | ||
|
|
07293e8d1e | ||
|
|
ca71d34076 | ||
|
|
548417761e | ||
|
|
7b8ad1d365 | ||
|
|
61cb6ec3dc | ||
|
|
349746f5f2 | ||
|
|
2e3b279873 | ||
|
|
f26861976d | ||
|
|
6bfeac7f80 | ||
|
|
a95fe588ca | ||
|
|
e5d11dd1a5 | ||
|
|
435e5c8a91 | ||
|
|
8d0553d9e6 | ||
|
|
9a239d1afb | ||
|
|
9252854f99 | ||
|
|
8d76e2679d | ||
|
|
4b1dcad7ae | ||
|
|
b45c386fd6 | ||
|
|
cb5fa79835 | ||
|
|
b74217bec2 | ||
|
|
fcf60e740d | ||
|
|
bb05600010 | ||
|
|
d3bb6d3988 | ||
|
|
f3945147a4 | ||
|
|
6398e92836 | ||
|
|
4d2b79156d | ||
|
|
b6d335f993 | ||
|
|
4b82c34b8f | ||
|
|
66fc852363 | ||
|
|
87274879a8 | ||
|
|
e4dbf8033c | ||
|
|
43db94d62d | ||
|
|
6d5fca2db1 | ||
|
|
d5e55448ef | ||
|
|
4ad998378f | ||
|
|
d46607c0d0 | ||
|
|
04920fa0bf | ||
|
|
1928da1fae | ||
|
|
06b051c53d | ||
|
|
473d765bb9 | ||
|
|
8e34c27b63 | ||
|
|
77aa2e940d | ||
|
|
3bbaf37193 | ||
|
|
b2d6ff9783 | ||
|
|
4fdde4f0e2 | ||
|
|
756768e745 | ||
|
|
83b791489b | ||
|
|
35132f9836 | ||
|
|
0e08785373 | ||
|
|
bf0dbdfd6a | ||
|
|
04407b8623 | ||
|
|
fb0ee34f10 | ||
|
|
ef63cfe8e4 | ||
|
|
e40f72e773 | ||
|
|
cec8ccb1a4 | ||
|
|
6a017efc0e | ||
|
|
18935440ed | ||
|
|
2ba6b3a2ab | ||
|
|
2438c6b7c2 | ||
|
|
32a84f1466 | ||
|
|
0002a895ca | ||
|
|
d0b43b187a | ||
|
|
33d381731f | ||
|
|
18f81d7824 | ||
|
|
844c8149d7 | ||
|
|
7617864ba5 | ||
|
|
58c234466c | ||
|
|
9071946e87 | ||
|
|
b24aa24f6a | ||
|
|
1fde234c78 | ||
|
|
afb9cba806 | ||
|
|
1c2f4866e2 | ||
|
|
e90ae2fb75 |
120
.coveragerc
120
.coveragerc
@@ -11,9 +11,15 @@ omit =
|
||||
homeassistant/components/alarmdecoder.py
|
||||
homeassistant/components/*/alarmdecoder.py
|
||||
|
||||
homeassistant/components/amcrest.py
|
||||
homeassistant/components/*/amcrest.py
|
||||
|
||||
homeassistant/components/apcupsd.py
|
||||
homeassistant/components/*/apcupsd.py
|
||||
|
||||
homeassistant/components/apple_tv.py
|
||||
homeassistant/components/*/apple_tv.py
|
||||
|
||||
homeassistant/components/arduino.py
|
||||
homeassistant/components/*/arduino.py
|
||||
|
||||
@@ -35,23 +41,35 @@ omit =
|
||||
homeassistant/components/bloomsky.py
|
||||
homeassistant/components/*/bloomsky.py
|
||||
|
||||
homeassistant/components/comfoconnect.py
|
||||
homeassistant/components/*/comfoconnect.py
|
||||
|
||||
homeassistant/components/digital_ocean.py
|
||||
homeassistant/components/*/digital_ocean.py
|
||||
|
||||
homeassistant/components/dweet.py
|
||||
homeassistant/components/*/dweet.py
|
||||
|
||||
homeassistant/components/eight_sleep.py
|
||||
homeassistant/components/*/eight_sleep.py
|
||||
|
||||
homeassistant/components/ecobee.py
|
||||
homeassistant/components/*/ecobee.py
|
||||
|
||||
homeassistant/components/enocean.py
|
||||
homeassistant/components/*/enocean.py
|
||||
|
||||
homeassistant/components/envisalink.py
|
||||
homeassistant/components/*/envisalink.py
|
||||
|
||||
homeassistant/components/google.py
|
||||
homeassistant/components/*/google.py
|
||||
|
||||
homeassistant/components/insteon_hub.py
|
||||
homeassistant/components/*/insteon_hub.py
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/*/hdmi_cec.py
|
||||
|
||||
homeassistant/components/homematic.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/insteon_local.py
|
||||
homeassistant/components/*/insteon_local.py
|
||||
@@ -65,12 +83,21 @@ omit =
|
||||
homeassistant/components/isy994.py
|
||||
homeassistant/components/*/isy994.py
|
||||
|
||||
homeassistant/components/joaoapps_join.py
|
||||
homeassistant/components/*/joaoapps_join.py
|
||||
|
||||
homeassistant/components/juicenet.py
|
||||
homeassistant/components/*/juicenet.py
|
||||
|
||||
homeassistant/components/kira.py
|
||||
homeassistant/components/*/kira.py
|
||||
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/*/knx.py
|
||||
|
||||
homeassistant/components/lametric.py
|
||||
homeassistant/components/*/lametric.py
|
||||
|
||||
homeassistant/components/lutron.py
|
||||
homeassistant/components/*/lutron.py
|
||||
|
||||
@@ -80,15 +107,27 @@ omit =
|
||||
homeassistant/components/mailgun.py
|
||||
homeassistant/components/*/mailgun.py
|
||||
|
||||
homeassistant/components/maxcube.py
|
||||
homeassistant/components/*/maxcube.py
|
||||
|
||||
homeassistant/components/mochad.py
|
||||
homeassistant/components/*/mochad.py
|
||||
|
||||
homeassistant/components/modbus.py
|
||||
homeassistant/components/*/modbus.py
|
||||
|
||||
homeassistant/components/mysensors.py
|
||||
homeassistant/components/*/mysensors.py
|
||||
|
||||
homeassistant/components/neato.py
|
||||
homeassistant/components/*/neato.py
|
||||
|
||||
homeassistant/components/nest.py
|
||||
homeassistant/components/*/nest.py
|
||||
|
||||
homeassistant/components/netatmo.py
|
||||
homeassistant/components/*/netatmo.py
|
||||
|
||||
homeassistant/components/octoprint.py
|
||||
homeassistant/components/*/octoprint.py
|
||||
|
||||
@@ -116,6 +155,9 @@ omit =
|
||||
homeassistant/components/scsgate.py
|
||||
homeassistant/components/*/scsgate.py
|
||||
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
homeassistant/components/tellduslive.py
|
||||
homeassistant/components/*/tellduslive.py
|
||||
|
||||
@@ -130,6 +172,12 @@ omit =
|
||||
homeassistant/components/twilio.py
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twilio_call.py
|
||||
|
||||
homeassistant/components/velbus.py
|
||||
homeassistant/components/*/velbus.py
|
||||
|
||||
homeassistant/components/velux.py
|
||||
homeassistant/components/*/velux.py
|
||||
|
||||
homeassistant/components/vera.py
|
||||
homeassistant/components/*/vera.py
|
||||
@@ -148,48 +196,25 @@ omit =
|
||||
homeassistant/components/wink.py
|
||||
homeassistant/components/*/wink.py
|
||||
|
||||
homeassistant/components/zigbee.py
|
||||
homeassistant/components/*/zigbee.py
|
||||
|
||||
homeassistant/components/enocean.py
|
||||
homeassistant/components/*/enocean.py
|
||||
|
||||
homeassistant/components/netatmo.py
|
||||
homeassistant/components/*/netatmo.py
|
||||
|
||||
homeassistant/components/neato.py
|
||||
homeassistant/components/*/neato.py
|
||||
|
||||
homeassistant/components/homematic.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/*/knx.py
|
||||
|
||||
homeassistant/components/zoneminder.py
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/mochad.py
|
||||
homeassistant/components/*/mochad.py
|
||||
homeassistant/components/xiaomi.py
|
||||
homeassistant/components/*/xiaomi.py
|
||||
|
||||
homeassistant/components/zabbix.py
|
||||
homeassistant/components/*/zabbix.py
|
||||
|
||||
homeassistant/components/maxcube.py
|
||||
homeassistant/components/*/maxcube.py
|
||||
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
homeassistant/components/zha/__init__.py
|
||||
homeassistant/components/zha/const.py
|
||||
homeassistant/components/*/zha.py
|
||||
|
||||
homeassistant/components/eight_sleep.py
|
||||
homeassistant/components/*/eight_sleep.py
|
||||
homeassistant/components/zigbee.py
|
||||
homeassistant/components/*/zigbee.py
|
||||
|
||||
homeassistant/components/zoneminder.py
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
homeassistant/components/alarm_control_panel/manual_mqtt.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
homeassistant/components/alarm_control_panel/totalconnect.py
|
||||
@@ -205,7 +230,6 @@ omit =
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/binary_sensor/tapsaff.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/amcrest.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/ffmpeg.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
@@ -224,11 +248,11 @@ omit =
|
||||
homeassistant/components/climate/sensibo.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
homeassistant/components/cover/knx.py
|
||||
homeassistant/components/cover/myq.py
|
||||
homeassistant/components/cover/opengarage.py
|
||||
homeassistant/components/cover/rpi_gpio.py
|
||||
homeassistant/components/cover/scsgate.py
|
||||
homeassistant/components/cover/wink.py
|
||||
homeassistant/components/device_tracker/actiontec.py
|
||||
homeassistant/components/device_tracker/aruba.py
|
||||
homeassistant/components/device_tracker/asuswrt.py
|
||||
@@ -242,6 +266,7 @@ omit =
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
homeassistant/components/device_tracker/linksys_ap.py
|
||||
homeassistant/components/device_tracker/linksys_smart.py
|
||||
homeassistant/components/device_tracker/luci.py
|
||||
homeassistant/components/device_tracker/mikrotik.py
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
@@ -256,19 +281,16 @@ omit =
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/trackr.py
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
homeassistant/components/device_tracker/xiaomi.py
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/emoncms_history.py
|
||||
homeassistant/components/emulated_hue/upnp.py
|
||||
homeassistant/components/fan/mqtt.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/foursquare.py
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/ifttt.py
|
||||
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/joaoapps_join.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/keyboard_remote.py
|
||||
homeassistant/components/light/avion.py
|
||||
@@ -278,7 +300,7 @@ omit =
|
||||
homeassistant/components/light/flux_led.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/hyperion.py
|
||||
homeassistant/components/light/lifx/*.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/lifx_legacy.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/mystrom.py
|
||||
@@ -287,6 +309,7 @@ omit =
|
||||
homeassistant/components/light/piglow.py
|
||||
homeassistant/components/light/sensehat.py
|
||||
homeassistant/components/light/tikteck.py
|
||||
homeassistant/components/light/tplink.py
|
||||
homeassistant/components/light/tradfri.py
|
||||
homeassistant/components/light/x10.py
|
||||
homeassistant/components/light/yeelight.py
|
||||
@@ -296,8 +319,8 @@ omit =
|
||||
homeassistant/components/lock/nuki.py
|
||||
homeassistant/components/lock/lockitron.py
|
||||
homeassistant/components/lock/sesame.py
|
||||
homeassistant/components/media_extractor.py
|
||||
homeassistant/components/media_player/anthemav.py
|
||||
homeassistant/components/media_player/apple_tv.py
|
||||
homeassistant/components/media_player/aquostv.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
@@ -312,7 +335,6 @@ omit =
|
||||
homeassistant/components/media_player/frontier_silicon.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
homeassistant/components/media_player/gstreamer.py
|
||||
homeassistant/components/media_player/hdmi_cec.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/lg_netcast.py
|
||||
@@ -335,6 +357,7 @@ omit =
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/spotify.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/vizio.py
|
||||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/volumio.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
@@ -342,13 +365,13 @@ omit =
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/ciscospark.py
|
||||
homeassistant/components/notify/clicksend.py
|
||||
homeassistant/components/notify/discord.py
|
||||
homeassistant/components/notify/facebook.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/gntp.py
|
||||
homeassistant/components/notify/group.py
|
||||
homeassistant/components/notify/instapush.py
|
||||
homeassistant/components/notify/joaoapps_join.py
|
||||
homeassistant/components/notify/kodi.py
|
||||
homeassistant/components/notify/lannouncer.py
|
||||
homeassistant/components/notify/llamalab_automate.py
|
||||
@@ -371,20 +394,22 @@ omit =
|
||||
homeassistant/components/notify/twitter.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
homeassistant/components/prometheus.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/scene/lifx_cloud.py
|
||||
homeassistant/components/sensor/amcrest.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
homeassistant/components/sensor/bh1750.py
|
||||
homeassistant/components/sensor/bitcoin.py
|
||||
homeassistant/components/sensor/blockchain.py
|
||||
homeassistant/components/sensor/bme280.py
|
||||
homeassistant/components/sensor/bom.py
|
||||
homeassistant/components/sensor/broadlink.py
|
||||
homeassistant/components/sensor/buienradar.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/citybikes.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/cert_expiry.py
|
||||
homeassistant/components/sensor/comed_hourly_pricing.py
|
||||
@@ -398,6 +423,7 @@ omit =
|
||||
homeassistant/components/sensor/dnsip.py
|
||||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/ebox.py
|
||||
homeassistant/components/sensor/eddystone_temperature.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
@@ -419,6 +445,7 @@ omit =
|
||||
homeassistant/components/sensor/haveibeenpwned.py
|
||||
homeassistant/components/sensor/hddtemp.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/htu21d.py
|
||||
homeassistant/components/sensor/hydroquebec.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
@@ -443,6 +470,7 @@ omit =
|
||||
homeassistant/components/sensor/openexchangerates.py
|
||||
homeassistant/components/sensor/opensky.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/otp.py
|
||||
homeassistant/components/sensor/pi_hole.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/pocketcasts.py
|
||||
@@ -473,6 +501,7 @@ omit =
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/upnp.py
|
||||
homeassistant/components/sensor/ups.py
|
||||
homeassistant/components/sensor/usps.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
@@ -480,6 +509,7 @@ omit =
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
homeassistant/components/sensor/yweather.py
|
||||
homeassistant/components/sensor/zamg.py
|
||||
homeassistant/components/shiftr.py
|
||||
homeassistant/components/spc.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
homeassistant/components/switch/anel_pwrctrl.py
|
||||
@@ -489,7 +519,6 @@ omit =
|
||||
homeassistant/components/switch/dlink.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/fritzdect.py
|
||||
homeassistant/components/switch/hdmi_cec.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
homeassistant/components/switch/hook.py
|
||||
homeassistant/components/switch/kankun.py
|
||||
@@ -503,6 +532,7 @@ omit =
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/switch/xiaomi_vacuum.py
|
||||
homeassistant/components/telegram_bot/*
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/amazon_polly.py
|
||||
|
||||
@@ -1,2 +1,14 @@
|
||||
.tox
|
||||
# General files
|
||||
.git
|
||||
.github
|
||||
config
|
||||
|
||||
# Test related files
|
||||
.tox
|
||||
|
||||
# Other virtualization methods
|
||||
venv
|
||||
.vagrant
|
||||
|
||||
# Temporary files
|
||||
**/__pycache__
|
||||
@@ -16,8 +16,8 @@ matrix:
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
- python: "3.6-dev"
|
||||
env: TOXENV=py36
|
||||
# - python: "3.6-dev"
|
||||
# env: TOXENV=py36
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=requirements
|
||||
# allow_failures:
|
||||
|
||||
41
CODEOWNERS
Normal file
41
CODEOWNERS
Normal file
@@ -0,0 +1,41 @@
|
||||
# People marked here will be automatically requested for a review
|
||||
# when the code that they own is touched.
|
||||
# https://github.com/blog/2392-introducing-code-owners
|
||||
|
||||
setup.py @home-assistant/core
|
||||
homeassistant/*.py @home-assistant/core
|
||||
homeassistant/helpers/* @home-assistant/core
|
||||
homeassistant/util/* @home-assistant/core
|
||||
homeassistant/components/api.py @home-assistant/core
|
||||
homeassistant/components/automation/* @home-assistant/core
|
||||
homeassistant/components/configurator.py @home-assistant/core
|
||||
homeassistant/components/group.py @home-assistant/core
|
||||
homeassistant/components/history.py @home-assistant/core
|
||||
homeassistant/components/http/* @home-assistant/core
|
||||
homeassistant/components/input_*.py @home-assistant/core
|
||||
homeassistant/components/introduction.py @home-assistant/core
|
||||
homeassistant/components/logger.py @home-assistant/core
|
||||
homeassistant/components/mqtt/* @home-assistant/core
|
||||
homeassistant/components/panel_custom.py @home-assistant/core
|
||||
homeassistant/components/panel_iframe.py @home-assistant/core
|
||||
homeassistant/components/persistent_notification.py @home-assistant/core
|
||||
homeassistant/components/scene/__init__.py @home-assistant/core
|
||||
homeassistant/components/scene/hass.py @home-assistant/core
|
||||
homeassistant/components/script.py @home-assistant/core
|
||||
homeassistant/components/shell_command.py @home-assistant/core
|
||||
homeassistant/components/sun.py @home-assistant/core
|
||||
homeassistant/components/updater.py @home-assistant/core
|
||||
homeassistant/components/weblink.py @home-assistant/core
|
||||
homeassistant/components/websocket_api.py @home-assistant/core
|
||||
homeassistant/components/zone.py @home-assistant/core
|
||||
|
||||
Dockerfile @home-assistant/docker
|
||||
virtualization/Docker/* @home-assistant/docker
|
||||
|
||||
homeassistant/components/zwave/* @home-assistant/z-wave
|
||||
homeassistant/components/*/zwave.py @home-assistant/z-wave
|
||||
|
||||
# Indiviudal components
|
||||
homeassistant/components/cover/template.py @PhracturedBlue
|
||||
homeassistant/components/device_tracker/automatic.py @armills
|
||||
homeassistant/components/media_player/kodi.py @armills
|
||||
10
Dockerfile
10
Dockerfile
@@ -1,3 +1,7 @@
|
||||
# Notice:
|
||||
# When updating this file, please also update virtualization/Docker/Dockerfile.dev
|
||||
# This way, the development image and the production image are kept in sync.
|
||||
|
||||
FROM python:3.6
|
||||
MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
|
||||
|
||||
@@ -21,8 +25,12 @@ RUN virtualization/Docker/setup_docker_prereqs
|
||||
|
||||
# Install hass component dependencies
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
|
||||
# Uninstall enum34 because some depenndecies 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
|
||||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet && \
|
||||
pip3 uninstall -y enum34
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Home Assistant |Build Status| |Coverage Status| |Join the chat at https://gitter.im/home-assistant/home-assistant| |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs|
|
||||
==============================================================================================================================================================================================
|
||||
Home Assistant |Build Status| |Coverage Status| |Chat Status|
|
||||
=============================================================
|
||||
|
||||
Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control.
|
||||
|
||||
@@ -31,6 +31,8 @@ of a component, check the `Home Assistant help section <https://home-assistant.i
|
||||
:target: https://travis-ci.org/home-assistant/home-assistant
|
||||
.. |Coverage Status| image:: https://img.shields.io/coveralls/home-assistant/home-assistant.svg
|
||||
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
|
||||
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
|
||||
:target: https://discord.gg/c5DvZ4e
|
||||
.. |Join the chat at https://gitter.im/home-assistant/home-assistant| image:: https://img.shields.io/badge/gitter-general-blue.svg
|
||||
:target: https://gitter.im/home-assistant/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
.. |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs| image:: https://img.shields.io/badge/gitter-development-yellowgreen.svg
|
||||
|
||||
@@ -229,8 +229,8 @@ def cmdline() -> List[str]:
|
||||
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
|
||||
return [sys.executable] + [arg for arg in sys.argv if
|
||||
arg != '--daemon']
|
||||
else:
|
||||
return [arg for arg in sys.argv if arg != '--daemon']
|
||||
|
||||
return [arg for arg in sys.argv if arg != '--daemon']
|
||||
|
||||
|
||||
def setup_and_run_hass(config_dir: str,
|
||||
|
||||
@@ -19,6 +19,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.util.logging import AsyncHandler
|
||||
from homeassistant.util.package import async_get_user_site, get_user_site
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.signal import async_register_signal_handling
|
||||
@@ -39,7 +40,7 @@ def from_config_dict(config: Dict[str, Any],
|
||||
skip_pip: bool=False,
|
||||
log_rotate_days: Any=None) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a config dict.
|
||||
"""Try to configure Home Assistant from a configuration dictionary.
|
||||
|
||||
Dynamically loads required components and its dependencies.
|
||||
"""
|
||||
@@ -48,7 +49,8 @@ def from_config_dict(config: Dict[str, Any],
|
||||
if config_dir is not None:
|
||||
config_dir = os.path.abspath(config_dir)
|
||||
hass.config.config_dir = config_dir
|
||||
mount_local_lib_path(config_dir)
|
||||
hass.loop.run_until_complete(
|
||||
async_mount_local_lib_path(config_dir, hass.loop))
|
||||
|
||||
# run task
|
||||
hass = hass.loop.run_until_complete(
|
||||
@@ -69,7 +71,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
skip_pip: bool=False,
|
||||
log_rotate_days: Any=None) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a config dict.
|
||||
"""Try to configure Home Assistant from a configuration dictionary.
|
||||
|
||||
Dynamically loads required components and its dependencies.
|
||||
This method is a coroutine.
|
||||
@@ -90,8 +92,8 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
|
||||
hass.config.skip_pip = skip_pip
|
||||
if skip_pip:
|
||||
_LOGGER.warning('Skipping pip installation of required modules. '
|
||||
'This may cause issues.')
|
||||
_LOGGER.warning("Skipping pip installation of required modules. "
|
||||
"This may cause issues")
|
||||
|
||||
if not loader.PREPARED:
|
||||
yield from hass.async_add_job(loader.prepare, hass)
|
||||
@@ -116,13 +118,13 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
# pylint: disable=not-an-iterable
|
||||
res = yield from core_components.async_setup(hass, config)
|
||||
if not res:
|
||||
_LOGGER.error('Home Assistant core failed to initialize. '
|
||||
'Further initialization aborted.')
|
||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
||||
"further initialization aborted")
|
||||
return hass
|
||||
|
||||
yield from persistent_notification.async_setup(hass, config)
|
||||
|
||||
_LOGGER.info('Home Assistant core initialized')
|
||||
_LOGGER.info("Home Assistant core initialized")
|
||||
|
||||
# stage 1
|
||||
for component in components:
|
||||
@@ -141,7 +143,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
yield from hass.async_block_till_done()
|
||||
|
||||
stop = time()
|
||||
_LOGGER.info('Home Assistant initialized in %.2fs', stop-start)
|
||||
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
|
||||
|
||||
async_register_signal_handling(hass)
|
||||
return hass
|
||||
@@ -183,7 +185,7 @@ def async_from_config_file(config_path: str,
|
||||
# Set config dir to directory holding config file
|
||||
config_dir = os.path.abspath(os.path.dirname(config_path))
|
||||
hass.config.config_dir = config_dir
|
||||
yield from hass.async_add_job(mount_local_lib_path, config_dir)
|
||||
yield from async_mount_local_lib_path(config_dir, hass.loop)
|
||||
|
||||
async_enable_logging(hass, verbose, log_rotate_days)
|
||||
|
||||
@@ -191,7 +193,7 @@ def async_from_config_file(config_path: str,
|
||||
config_dict = yield from hass.async_add_job(
|
||||
conf_util.load_yaml_config_file, config_path)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error('Error loading %s: %s', config_path, err)
|
||||
_LOGGER.error("Error loading %s: %s", config_path, err)
|
||||
return None
|
||||
finally:
|
||||
clear_secret_cache()
|
||||
@@ -276,11 +278,23 @@ def async_enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
|
||||
|
||||
def mount_local_lib_path(config_dir: str) -> str:
|
||||
"""Add local library to Python Path."""
|
||||
deps_dir = os.path.join(config_dir, 'deps')
|
||||
lib_dir = get_user_site(deps_dir)
|
||||
if lib_dir not in sys.path:
|
||||
sys.path.insert(0, lib_dir)
|
||||
return deps_dir
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_mount_local_lib_path(config_dir: str,
|
||||
loop: asyncio.AbstractEventLoop) -> str:
|
||||
"""Add local library to Python Path.
|
||||
|
||||
Async friendly.
|
||||
This function is a coroutine.
|
||||
"""
|
||||
deps_dir = os.path.join(config_dir, 'deps')
|
||||
if deps_dir not in sys.path:
|
||||
sys.path.insert(0, os.path.join(config_dir, 'deps'))
|
||||
lib_dir = yield from async_get_user_site(deps_dir, loop=loop)
|
||||
if lib_dir not in sys.path:
|
||||
sys.path.insert(0, lib_dir)
|
||||
return deps_dir
|
||||
|
||||
@@ -15,7 +15,6 @@ import homeassistant.core as ha
|
||||
import homeassistant.config as conf_util
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.service import extract_entity_ids
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
|
||||
SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART,
|
||||
@@ -33,25 +32,27 @@ def is_on(hass, entity_id=None):
|
||||
If there is no entity id given we will check all.
|
||||
"""
|
||||
if entity_id:
|
||||
group = get_component('group')
|
||||
|
||||
entity_ids = group.expand_entity_ids(hass, [entity_id])
|
||||
entity_ids = hass.components.group.expand_entity_ids([entity_id])
|
||||
else:
|
||||
entity_ids = hass.states.entity_ids()
|
||||
|
||||
for entity_id in entity_ids:
|
||||
domain = ha.split_entity_id(entity_id)[0]
|
||||
|
||||
module = get_component(domain)
|
||||
for ent_id in entity_ids:
|
||||
domain = ha.split_entity_id(ent_id)[0]
|
||||
|
||||
try:
|
||||
if module.is_on(hass, entity_id):
|
||||
return True
|
||||
component = getattr(hass.components, domain)
|
||||
|
||||
except AttributeError:
|
||||
# module is None or method is_on does not exist
|
||||
_LOGGER.exception("Failed to call %s.is_on for %s",
|
||||
module, entity_id)
|
||||
except ImportError:
|
||||
_LOGGER.error('Failed to call %s.is_on: component not found',
|
||||
domain)
|
||||
continue
|
||||
|
||||
if not hasattr(component, 'is_on'):
|
||||
_LOGGER.warning("Component %s has no is_on method.", domain)
|
||||
continue
|
||||
|
||||
if component.is_on(ent_id):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -161,10 +162,9 @@ def async_setup(hass, config):
|
||||
return
|
||||
|
||||
if errors:
|
||||
notif = get_component('persistent_notification')
|
||||
_LOGGER.error(errors)
|
||||
notif.async_create(
|
||||
hass, "Config error. See dev-info panel for details.",
|
||||
hass.components.persistent_notification.async_create(
|
||||
"Config error. See dev-info panel for details.",
|
||||
"Config validating", "{0}.check_config".format(ha.DOMAIN))
|
||||
return
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
|
||||
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -44,6 +45,7 @@ ALARM_SERVICE_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@bind_hass
|
||||
def alarm_disarm(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for disarm."""
|
||||
data = {}
|
||||
@@ -55,6 +57,7 @@ def alarm_disarm(hass, code=None, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def alarm_arm_home(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for arm home."""
|
||||
data = {}
|
||||
@@ -66,6 +69,7 @@ def alarm_arm_home(hass, code=None, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def alarm_arm_away(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for arm away."""
|
||||
data = {}
|
||||
@@ -77,6 +81,7 @@ def alarm_arm_away(hass, code=None, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def alarm_trigger(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for trigger."""
|
||||
data = {}
|
||||
|
||||
@@ -92,8 +92,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif self._alarm.state.lower() == 'armed away':
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
|
||||
@@ -113,8 +113,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
"""Regex for code format or None if no code is required."""
|
||||
if self._code:
|
||||
return None
|
||||
else:
|
||||
return '^\\d{4,6}$'
|
||||
return '^\\d{4,6}$'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -99,8 +99,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
self._trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
return self._pre_trigger_state
|
||||
return self._pre_trigger_state
|
||||
|
||||
return self._state
|
||||
|
||||
|
||||
235
homeassistant/components/alarm_control_panel/manual_mqtt.py
Normal file
235
homeassistant/components/alarm_control_panel/manual_mqtt.py
Normal file
@@ -0,0 +1,235 @@
|
||||
"""
|
||||
Support for manual alarms controllable via MQTT.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.manual_mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, CONF_PLATFORM,
|
||||
CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME,
|
||||
CONF_DISARM_AFTER_TRIGGER)
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.core import callback
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
|
||||
CONF_PAYLOAD_DISARM = 'payload_disarm'
|
||||
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
|
||||
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
|
||||
|
||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
||||
DEFAULT_PENDING_TIME = 60
|
||||
DEFAULT_TRIGGER_TIME = 120
|
||||
DEFAULT_DISARM_AFTER_TRIGGER = False
|
||||
DEFAULT_ARM_AWAY = 'ARM_AWAY'
|
||||
DEFAULT_ARM_HOME = 'ARM_HOME'
|
||||
DEFAULT_DISARM = 'DISARM'
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PLATFORM): 'manual_mqtt',
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
|
||||
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
|
||||
vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the manual MQTT alarm platform."""
|
||||
add_devices([ManualMQTTAlarm(
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
config.get(CONF_CODE),
|
||||
config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME),
|
||||
config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME),
|
||||
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
|
||||
config.get(mqtt.CONF_STATE_TOPIC),
|
||||
config.get(mqtt.CONF_COMMAND_TOPIC),
|
||||
config.get(mqtt.CONF_QOS),
|
||||
config.get(CONF_PAYLOAD_DISARM),
|
||||
config.get(CONF_PAYLOAD_ARM_HOME),
|
||||
config.get(CONF_PAYLOAD_ARM_AWAY))])
|
||||
|
||||
|
||||
class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
"""
|
||||
Representation of an alarm status.
|
||||
|
||||
When armed, will be pending for 'pending_time', after that armed.
|
||||
When triggered, will be pending for 'trigger_time'. After that will be
|
||||
triggered for 'trigger_time', after that we return to the previous state
|
||||
or disarm if `disarm_after_trigger` is true.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, name, code, pending_time,
|
||||
trigger_time, disarm_after_trigger,
|
||||
state_topic, command_topic, qos,
|
||||
payload_disarm, payload_arm_home, payload_arm_away):
|
||||
"""Init the manual MQTT alarm panel."""
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._pending_time = datetime.timedelta(seconds=pending_time)
|
||||
self._trigger_time = datetime.timedelta(seconds=trigger_time)
|
||||
self._disarm_after_trigger = disarm_after_trigger
|
||||
self._pre_trigger_state = self._state
|
||||
self._state_ts = None
|
||||
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
self._qos = qos
|
||||
self._payload_disarm = payload_disarm
|
||||
self._payload_arm_home = payload_arm_home
|
||||
self._payload_arm_away = payload_arm_away
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._state in (STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY) and \
|
||||
self._pending_time and self._state_ts + self._pending_time > \
|
||||
dt_util.utcnow():
|
||||
return STATE_ALARM_PENDING
|
||||
|
||||
if self._state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
if self._state_ts + self._pending_time > dt_util.utcnow():
|
||||
return STATE_ALARM_PENDING
|
||||
elif (self._state_ts + self._pending_time +
|
||||
self._trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
return self._pre_trigger_state
|
||||
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""One or more characters."""
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, STATE_ALARM_DISARMED):
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_HOME):
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_AWAY):
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command. No code needed."""
|
||||
self._pre_trigger_state = self._state
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._trigger_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time + self._trigger_time)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
return check
|
||||
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
async_track_state_change(
|
||||
self.hass, self.entity_id, self._async_state_changed_listener
|
||||
)
|
||||
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""Run when new MQTT message has been received."""
|
||||
if payload == self._payload_disarm:
|
||||
self.async_alarm_disarm(self._code)
|
||||
elif payload == self._payload_arm_home:
|
||||
self.async_alarm_arm_home(self._code)
|
||||
elif payload == self._payload_arm_away:
|
||||
self.async_alarm_arm_away(self._code)
|
||||
else:
|
||||
_LOGGER.warning("Received unexpected payload: %s", payload)
|
||||
return
|
||||
|
||||
return mqtt.async_subscribe(
|
||||
self.hass, self._command_topic, message_received, self._qos)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_state_changed_listener(self, entity_id, old_state, new_state):
|
||||
"""Publish state change to MQTT."""
|
||||
mqtt.async_publish(self.hass, self._state_topic, new_state.state,
|
||||
self._qos, True)
|
||||
@@ -15,9 +15,8 @@ from homeassistant.const import (
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.loader as loader
|
||||
|
||||
REQUIREMENTS = ['simplisafe-python==1.0.2']
|
||||
REQUIREMENTS = ['simplisafe-python==1.0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -42,7 +41,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
simplisafe = SimpliSafeApiInterface()
|
||||
status = simplisafe.set_credentials(username, password)
|
||||
if status:
|
||||
@@ -53,8 +51,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
else:
|
||||
message = 'Failed to log into SimpliSafe. Check credentials.'
|
||||
_LOGGER.error(message)
|
||||
persistent_notification.create(
|
||||
hass, message,
|
||||
hass.components.persistent_notification.create(
|
||||
message,
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
@@ -80,8 +78,7 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
"""Return the name of the device."""
|
||||
if self._name is not None:
|
||||
return self._name
|
||||
else:
|
||||
return 'Alarm {}'.format(self.simplisafe.location_id())
|
||||
return 'Alarm {}'.format(self.simplisafe.location_id())
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN,
|
||||
CONF_NAME)
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.7']
|
||||
REQUIREMENTS = ['total_connect_client==0.11']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.verisure/
|
||||
"""
|
||||
import logging
|
||||
from time import sleep
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.verisure import HUB as hub
|
||||
@@ -20,20 +21,29 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Verisure platform."""
|
||||
alarms = []
|
||||
if int(hub.config.get(CONF_ALARM, 1)):
|
||||
hub.update_alarms()
|
||||
alarms.extend([
|
||||
VerisureAlarm(value.id)
|
||||
for value in hub.alarm_status.values()
|
||||
])
|
||||
hub.update_overview()
|
||||
alarms.append(VerisureAlarm())
|
||||
add_devices(alarms)
|
||||
|
||||
|
||||
def set_arm_state(state, code=None):
|
||||
"""Send set arm state command."""
|
||||
transaction_id = hub.session.set_arm_state(code, state)[
|
||||
'armStateChangeTransactionId']
|
||||
_LOGGER.info('verisure set arm state %s', state)
|
||||
transaction = {}
|
||||
while 'result' not in transaction:
|
||||
sleep(0.5)
|
||||
transaction = hub.session.get_arm_state_transaction(transaction_id)
|
||||
# pylint: disable=unexpected-keyword-arg
|
||||
hub.update_overview(no_throttle=True)
|
||||
|
||||
|
||||
class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation of a Verisure alarm status."""
|
||||
|
||||
def __init__(self, device_id):
|
||||
"""Initialize the Verisure alarm panel."""
|
||||
self._id = device_id
|
||||
def __init__(self):
|
||||
"""Initalize the Verisure alarm panel."""
|
||||
self._state = STATE_UNKNOWN
|
||||
self._digits = hub.config.get(CONF_CODE_DIGITS)
|
||||
self._changed_by = None
|
||||
@@ -41,18 +51,13 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return 'Alarm {}'.format(self._id)
|
||||
return '{} alarm'.format(hub.session.installations[0]['alias'])
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return hub.available
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the code format as regex."""
|
||||
@@ -65,33 +70,26 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def update(self):
|
||||
"""Update alarm status."""
|
||||
hub.update_alarms()
|
||||
|
||||
if hub.alarm_status[self._id].status == 'unarmed':
|
||||
hub.update_overview()
|
||||
status = hub.get_first("$.armState.statusType")
|
||||
if status == 'DISARMED':
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif hub.alarm_status[self._id].status == 'armedhome':
|
||||
elif status == 'ARMED_HOME':
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif hub.alarm_status[self._id].status == 'armed':
|
||||
elif status == 'ARMED_AWAY':
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
elif hub.alarm_status[self._id].status != 'pending':
|
||||
_LOGGER.error(
|
||||
"Unknown alarm state %s", hub.alarm_status[self._id].status)
|
||||
self._changed_by = hub.alarm_status[self._id].name
|
||||
elif status != 'PENDING':
|
||||
_LOGGER.error('Unknown alarm state %s', status)
|
||||
self._changed_by = hub.get_first("$.armState.name")
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
hub.my_pages.alarm.set(code, 'DISARMED')
|
||||
_LOGGER.info("Verisure alarm disarming")
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
set_arm_state('DISARMED', code)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
hub.my_pages.alarm.set(code, 'ARMED_HOME')
|
||||
_LOGGER.info("Verisure alarm arming home")
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
set_arm_state('ARMED_HOME', code)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
hub.my_pages.alarm.set(code, 'ARMED_AWAY')
|
||||
_LOGGER.info("Verisure alarm arming away")
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
set_arm_state('ARMED_AWAY', code)
|
||||
|
||||
@@ -39,10 +39,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
|
||||
"""Representation a Wink camera alarm."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink alarm."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
REQUIREMENTS = ['alarmdecoder==0.12.1.0']
|
||||
REQUIREMENTS = ['alarmdecoder==0.12.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ _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'
|
||||
@@ -35,6 +36,7 @@ DEFAULT_SKIP_FIRST = False
|
||||
|
||||
ALERT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_DONE_MESSAGE, default=None): 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)]),
|
||||
@@ -121,10 +123,10 @@ def async_setup(hass, config):
|
||||
# Setup alerts
|
||||
for entity_id, alert in alerts.items():
|
||||
entity = Alert(hass, entity_id,
|
||||
alert[CONF_NAME], alert[CONF_ENTITY_ID],
|
||||
alert[CONF_STATE], alert[CONF_REPEAT],
|
||||
alert[CONF_SKIP_FIRST], alert[CONF_NOTIFIERS],
|
||||
alert[CONF_CAN_ACK])
|
||||
alert[CONF_NAME], alert[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
|
||||
|
||||
# Read descriptions
|
||||
@@ -154,8 +156,8 @@ def async_setup(hass, config):
|
||||
class Alert(ToggleEntity):
|
||||
"""Representation of an alert."""
|
||||
|
||||
def __init__(self, hass, entity_id, name, watched_entity_id, state,
|
||||
repeat, skip_first, notifiers, can_ack):
|
||||
def __init__(self, hass, entity_id, name, done_message, watched_entity_id,
|
||||
state, repeat, skip_first, notifiers, can_ack):
|
||||
"""Initialize the alert."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
@@ -163,6 +165,7 @@ class Alert(ToggleEntity):
|
||||
self._skip_first = skip_first
|
||||
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
|
||||
@@ -170,6 +173,7 @@ class Alert(ToggleEntity):
|
||||
self._firing = False
|
||||
self._ack = False
|
||||
self._cancel = None
|
||||
self._send_done_message = False
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(entity_id)
|
||||
|
||||
event.async_track_state_change(
|
||||
@@ -230,6 +234,8 @@ class Alert(ToggleEntity):
|
||||
self._cancel()
|
||||
self._ack = False
|
||||
self._firing = False
|
||||
if self._done_message and self._send_done_message:
|
||||
yield from self._notify_done_message()
|
||||
self.hass.async_add_job(self.async_update_ha_state)
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -249,20 +255,30 @@ class Alert(ToggleEntity):
|
||||
|
||||
if not self._ack:
|
||||
_LOGGER.info("Alerting: %s", self._name)
|
||||
self._send_done_message = True
|
||||
for target in self._notifiers:
|
||||
yield from self.hass.services.async_call(
|
||||
'notify', target, {'message': self._name})
|
||||
yield from self._schedule_notify()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self):
|
||||
def _notify_done_message(self, *args):
|
||||
"""Send notification of complete alert."""
|
||||
_LOGGER.info("Alerting: %s", self._done_message)
|
||||
self._send_done_message = False
|
||||
for target in self._notifiers:
|
||||
yield from self.hass.services.async_call(
|
||||
'notify', target, {'message': self._done_message})
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
"""Async Unacknowledge alert."""
|
||||
_LOGGER.debug("Reset Alert: %s", self._name)
|
||||
self._ack = False
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self):
|
||||
def async_turn_off(self, **kwargs):
|
||||
"""Async Acknowledge alert."""
|
||||
_LOGGER.debug("Acknowledged Alert: %s", self._name)
|
||||
self._ack = True
|
||||
|
||||
@@ -15,8 +15,8 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import template, script, config_validation as cv
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.helpers import intent, template, config_validation as cv
|
||||
from homeassistant.components import http
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -60,6 +60,12 @@ class SpeechType(enum.Enum):
|
||||
ssml = "SSML"
|
||||
|
||||
|
||||
SPEECH_MAPPINGS = {
|
||||
'plain': SpeechType.plaintext,
|
||||
'ssml': SpeechType.ssml,
|
||||
}
|
||||
|
||||
|
||||
class CardType(enum.Enum):
|
||||
"""The Alexa card types."""
|
||||
|
||||
@@ -69,20 +75,6 @@ class CardType(enum.Enum):
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {
|
||||
CONF_INTENTS: {
|
||||
cv.string: {
|
||||
vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_CARD): {
|
||||
vol.Required(CONF_TYPE): cv.enum(CardType),
|
||||
vol.Required(CONF_TITLE): cv.template,
|
||||
vol.Required(CONF_CONTENT): cv.template,
|
||||
},
|
||||
vol.Optional(CONF_SPEECH): {
|
||||
vol.Required(CONF_TYPE): cv.enum(SpeechType),
|
||||
vol.Required(CONF_TEXT): cv.template,
|
||||
}
|
||||
}
|
||||
},
|
||||
CONF_FLASH_BRIEFINGS: {
|
||||
cv.string: vol.All(cv.ensure_list, [{
|
||||
vol.Required(CONF_UID, default=str(uuid.uuid4())): cv.string,
|
||||
@@ -96,40 +88,27 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Activate Alexa component."""
|
||||
intents = config[DOMAIN].get(CONF_INTENTS, {})
|
||||
flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {})
|
||||
|
||||
hass.http.register_view(AlexaIntentsView(hass, intents))
|
||||
hass.http.register_view(AlexaIntentsView)
|
||||
hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefings))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AlexaIntentsView(HomeAssistantView):
|
||||
class AlexaIntentsView(http.HomeAssistantView):
|
||||
"""Handle Alexa requests."""
|
||||
|
||||
url = INTENTS_API_ENDPOINT
|
||||
name = 'api:alexa'
|
||||
|
||||
def __init__(self, hass, intents):
|
||||
"""Initialize Alexa view."""
|
||||
super().__init__()
|
||||
|
||||
intents = copy.deepcopy(intents)
|
||||
template.attach(hass, intents)
|
||||
|
||||
for name, intent in intents.items():
|
||||
if CONF_ACTION in intent:
|
||||
intent[CONF_ACTION] = script.Script(
|
||||
hass, intent[CONF_ACTION], "Alexa intent {}".format(name))
|
||||
|
||||
self.intents = intents
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Handle Alexa."""
|
||||
hass = request.app['hass']
|
||||
data = yield from request.json()
|
||||
|
||||
_LOGGER.debug('Received Alexa request: %s', data)
|
||||
@@ -146,14 +125,14 @@ class AlexaIntentsView(HomeAssistantView):
|
||||
if req_type == 'SessionEndedRequest':
|
||||
return None
|
||||
|
||||
intent = req.get('intent')
|
||||
response = AlexaResponse(request.app['hass'], intent)
|
||||
alexa_intent_info = req.get('intent')
|
||||
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
||||
|
||||
if req_type == 'LaunchRequest':
|
||||
response.add_speech(
|
||||
alexa_response.add_speech(
|
||||
SpeechType.plaintext,
|
||||
"Hello, and welcome to the future. How may I help?")
|
||||
return self.json(response)
|
||||
return self.json(alexa_response)
|
||||
|
||||
if req_type != 'IntentRequest':
|
||||
_LOGGER.warning('Received unsupported request: %s', req_type)
|
||||
@@ -161,38 +140,47 @@ class AlexaIntentsView(HomeAssistantView):
|
||||
'Received unsupported request: {}'.format(req_type),
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
intent_name = intent['name']
|
||||
config = self.intents.get(intent_name)
|
||||
intent_name = alexa_intent_info['name']
|
||||
|
||||
if config is None:
|
||||
try:
|
||||
intent_response = yield from intent.async_handle(
|
||||
hass, DOMAIN, intent_name,
|
||||
{key: {'value': value} for key, value
|
||||
in alexa_response.variables.items()})
|
||||
except intent.UnknownIntent as err:
|
||||
_LOGGER.warning('Received unknown intent %s', intent_name)
|
||||
response.add_speech(
|
||||
alexa_response.add_speech(
|
||||
SpeechType.plaintext,
|
||||
"This intent is not yet configured within Home Assistant.")
|
||||
return self.json(response)
|
||||
return self.json(alexa_response)
|
||||
|
||||
speech = config.get(CONF_SPEECH)
|
||||
card = config.get(CONF_CARD)
|
||||
action = config.get(CONF_ACTION)
|
||||
except intent.InvalidSlotInfo as err:
|
||||
_LOGGER.error('Received invalid slot data from Alexa: %s', err)
|
||||
return self.json_message('Invalid slot data received',
|
||||
HTTP_BAD_REQUEST)
|
||||
except intent.IntentError:
|
||||
_LOGGER.exception('Error handling request for %s', intent_name)
|
||||
return self.json_message('Error handling intent', HTTP_BAD_REQUEST)
|
||||
|
||||
if action is not None:
|
||||
yield from action.async_run(response.variables)
|
||||
for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
|
||||
if intent_speech in intent_response.speech:
|
||||
alexa_response.add_speech(
|
||||
alexa_speech,
|
||||
intent_response.speech[intent_speech]['speech'])
|
||||
break
|
||||
|
||||
# pylint: disable=unsubscriptable-object
|
||||
if speech is not None:
|
||||
response.add_speech(speech[CONF_TYPE], speech[CONF_TEXT])
|
||||
if 'simple' in intent_response.card:
|
||||
alexa_response.add_card(
|
||||
CardType.simple, intent_response.card['simple']['title'],
|
||||
intent_response.card['simple']['content'])
|
||||
|
||||
if card is not None:
|
||||
response.add_card(card[CONF_TYPE], card[CONF_TITLE],
|
||||
card[CONF_CONTENT])
|
||||
|
||||
return self.json(response)
|
||||
return self.json(alexa_response)
|
||||
|
||||
|
||||
class AlexaResponse(object):
|
||||
"""Help generating the response for Alexa."""
|
||||
|
||||
def __init__(self, hass, intent=None):
|
||||
def __init__(self, hass, intent_info):
|
||||
"""Initialize the response."""
|
||||
self.hass = hass
|
||||
self.speech = None
|
||||
@@ -201,8 +189,9 @@ class AlexaResponse(object):
|
||||
self.session_attributes = {}
|
||||
self.should_end_session = True
|
||||
self.variables = {}
|
||||
if intent is not None and 'slots' in intent:
|
||||
for key, value in intent['slots'].items():
|
||||
# Intent is None if request was a LaunchRequest or SessionEndedRequest
|
||||
if intent_info is not None:
|
||||
for key, value in intent_info.get('slots', {}).items():
|
||||
if 'value' in value:
|
||||
underscored_key = key.replace('.', '_')
|
||||
self.variables[underscored_key] = value['value']
|
||||
@@ -219,8 +208,8 @@ class AlexaResponse(object):
|
||||
self.card = card
|
||||
return
|
||||
|
||||
card["title"] = title.async_render(self.variables)
|
||||
card["content"] = content.async_render(self.variables)
|
||||
card["title"] = title
|
||||
card["content"] = content
|
||||
self.card = card
|
||||
|
||||
def add_speech(self, speech_type, text):
|
||||
@@ -229,9 +218,6 @@ class AlexaResponse(object):
|
||||
|
||||
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
|
||||
|
||||
if isinstance(text, template.Template):
|
||||
text = text.async_render(self.variables)
|
||||
|
||||
self.speech = {
|
||||
'type': speech_type.value,
|
||||
key: text
|
||||
@@ -272,7 +258,7 @@ class AlexaResponse(object):
|
||||
}
|
||||
|
||||
|
||||
class AlexaFlashBriefingView(HomeAssistantView):
|
||||
class AlexaFlashBriefingView(http.HomeAssistantView):
|
||||
"""Handle Alexa Flash Briefing skill requests."""
|
||||
|
||||
url = FLASH_BRIEFINGS_API_ENDPOINT
|
||||
|
||||
147
homeassistant/components/amcrest.py
Normal file
147
homeassistant/components/amcrest.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
This component provides basic support for Amcrest IP cameras.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/amcrest/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_SENSORS, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.2.1']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_AUTHENTICATION = 'authentication'
|
||||
CONF_RESOLUTION = 'resolution'
|
||||
CONF_STREAM_SOURCE = 'stream_source'
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
|
||||
DEFAULT_NAME = 'Amcrest Camera'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_RESOLUTION = 'high'
|
||||
DEFAULT_STREAM_SOURCE = 'snapshot'
|
||||
TIMEOUT = 10
|
||||
|
||||
DATA_AMCREST = 'amcrest'
|
||||
DOMAIN = 'amcrest'
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
|
||||
RESOLUTION_LIST = {
|
||||
'high': 0,
|
||||
'low': 1,
|
||||
}
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
AUTHENTICATION_LIST = {
|
||||
'basic': 'basic'
|
||||
}
|
||||
|
||||
STREAM_SOURCE_LIST = {
|
||||
'mjpeg': 0,
|
||||
'snapshot': 1,
|
||||
'rtsp': 2,
|
||||
}
|
||||
|
||||
# Sensor types are defined like: Name, units, icon
|
||||
SENSORS = {
|
||||
'motion_detector': ['Motion Detected', None, 'mdi:run'],
|
||||
'sdcard': ['SD Used', '%', 'mdi:sd'],
|
||||
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
|
||||
vol.All(vol.In(AUTHENTICATION_LIST)),
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
|
||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Optional(CONF_SENSORS, default=None):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Amcrest IP Camera component."""
|
||||
from amcrest import AmcrestCamera
|
||||
|
||||
amcrest_cams = config[DOMAIN]
|
||||
|
||||
for device in amcrest_cams:
|
||||
camera = AmcrestCamera(device.get(CONF_HOST),
|
||||
device.get(CONF_PORT),
|
||||
device.get(CONF_USERNAME),
|
||||
device.get(CONF_PASSWORD)).camera
|
||||
try:
|
||||
camera.current_time
|
||||
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
|
||||
name = device.get(CONF_NAME)
|
||||
resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)]
|
||||
sensors = device.get(CONF_SENSORS)
|
||||
stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)]
|
||||
|
||||
username = device.get(CONF_USERNAME)
|
||||
password = device.get(CONF_PASSWORD)
|
||||
|
||||
# currently aiohttp only works with basic authentication
|
||||
# only valid for mjpeg streaming
|
||||
if username is not None and password is not None:
|
||||
if device.get(CONF_AUTHENTICATION) == HTTP_BASIC_AUTHENTICATION:
|
||||
authentication = aiohttp.BasicAuth(username, password)
|
||||
else:
|
||||
authentication = None
|
||||
|
||||
discovery.load_platform(
|
||||
hass, 'camera', DOMAIN, {
|
||||
'device': camera,
|
||||
CONF_AUTHENTICATION: authentication,
|
||||
CONF_FFMPEG_ARGUMENTS: ffmpeg_arguments,
|
||||
CONF_NAME: name,
|
||||
CONF_RESOLUTION: resolution,
|
||||
CONF_STREAM_SOURCE: stream_source,
|
||||
}, config)
|
||||
|
||||
if sensors:
|
||||
discovery.load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
'device': camera,
|
||||
CONF_NAME: name,
|
||||
CONF_SENSORS: sensors,
|
||||
}, config)
|
||||
|
||||
return True
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['apcaccess==0.0.10']
|
||||
REQUIREMENTS = ['apcaccess==0.0.13']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -198,8 +198,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
state = request.app['hass'].states.get(entity_id)
|
||||
if state:
|
||||
return self.json(state)
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, entity_id):
|
||||
@@ -213,7 +212,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
|
||||
new_state = data.get('state')
|
||||
|
||||
if not new_state:
|
||||
if new_state is None:
|
||||
return self.json_message('No state specified', HTTP_BAD_REQUEST)
|
||||
|
||||
attributes = data.get('attributes')
|
||||
@@ -237,8 +236,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
"""Remove entity."""
|
||||
if request.app['hass'].states.async_remove(entity_id):
|
||||
return self.json_message('Entity removed')
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
|
||||
class APIEventListenersView(HomeAssistantView):
|
||||
|
||||
@@ -5,13 +5,12 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/apiai/
|
||||
"""
|
||||
import asyncio
|
||||
import copy
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import PROJECT_NAME, HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import template, script, config_validation as cv
|
||||
from homeassistant.helpers import intent, template
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -29,24 +28,14 @@ DOMAIN = 'apiai'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {
|
||||
CONF_INTENTS: {
|
||||
cv.string: {
|
||||
vol.Optional(CONF_SPEECH): cv.template,
|
||||
vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_ASYNC_ACTION,
|
||||
default=DEFAULT_CONF_ASYNC_ACTION): cv.boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
DOMAIN: {}
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Activate API.AI component."""
|
||||
intents = config[DOMAIN].get(CONF_INTENTS, {})
|
||||
|
||||
hass.http.register_view(ApiaiIntentsView(hass, intents))
|
||||
hass.http.register_view(ApiaiIntentsView)
|
||||
|
||||
return True
|
||||
|
||||
@@ -57,24 +46,10 @@ class ApiaiIntentsView(HomeAssistantView):
|
||||
url = INTENTS_API_ENDPOINT
|
||||
name = 'api:apiai'
|
||||
|
||||
def __init__(self, hass, intents):
|
||||
"""Initialize API.AI view."""
|
||||
super().__init__()
|
||||
|
||||
self.hass = hass
|
||||
intents = copy.deepcopy(intents)
|
||||
template.attach(hass, intents)
|
||||
|
||||
for name, intent in intents.items():
|
||||
if CONF_ACTION in intent:
|
||||
intent[CONF_ACTION] = script.Script(
|
||||
hass, intent[CONF_ACTION], "Apiai intent {}".format(name))
|
||||
|
||||
self.intents = intents
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Handle API.AI."""
|
||||
hass = request.app['hass']
|
||||
data = yield from request.json()
|
||||
|
||||
_LOGGER.debug("Received api.ai request: %s", data)
|
||||
@@ -91,55 +66,41 @@ class ApiaiIntentsView(HomeAssistantView):
|
||||
if action_incomplete:
|
||||
return None
|
||||
|
||||
# use intent to no mix HASS actions with this parameter
|
||||
intent = req.get('action')
|
||||
action = req.get('action')
|
||||
parameters = req.get('parameters')
|
||||
# contexts = req.get('contexts')
|
||||
response = ApiaiResponse(parameters)
|
||||
apiai_response = ApiaiResponse(parameters)
|
||||
|
||||
# Default Welcome Intent
|
||||
# Maybe is better to handle this in api.ai directly?
|
||||
#
|
||||
# if intent == 'input.welcome':
|
||||
# response.add_speech(
|
||||
# "Hello, and welcome to the future. How may I help?")
|
||||
# return self.json(response)
|
||||
|
||||
if intent == "":
|
||||
if action == "":
|
||||
_LOGGER.warning("Received intent with empty action")
|
||||
response.add_speech(
|
||||
apiai_response.add_speech(
|
||||
"You have not defined an action in your api.ai intent.")
|
||||
return self.json(response)
|
||||
return self.json(apiai_response)
|
||||
|
||||
config = self.intents.get(intent)
|
||||
try:
|
||||
intent_response = yield from intent.async_handle(
|
||||
hass, DOMAIN, action,
|
||||
{key: {'value': value} for key, value
|
||||
in parameters.items()})
|
||||
|
||||
if config is None:
|
||||
_LOGGER.warning("Received unknown intent %s", intent)
|
||||
response.add_speech(
|
||||
"Intent '%s' is not yet configured within Home Assistant." %
|
||||
intent)
|
||||
return self.json(response)
|
||||
except intent.UnknownIntent as err:
|
||||
_LOGGER.warning('Received unknown intent %s', action)
|
||||
apiai_response.add_speech(
|
||||
"This intent is not yet configured within Home Assistant.")
|
||||
return self.json(apiai_response)
|
||||
|
||||
speech = config.get(CONF_SPEECH)
|
||||
action = config.get(CONF_ACTION)
|
||||
async_action = config.get(CONF_ASYNC_ACTION)
|
||||
except intent.InvalidSlotInfo as err:
|
||||
_LOGGER.error('Received invalid slot data: %s', err)
|
||||
return self.json_message('Invalid slot data received',
|
||||
HTTP_BAD_REQUEST)
|
||||
except intent.IntentError:
|
||||
_LOGGER.exception('Error handling request for %s', action)
|
||||
return self.json_message('Error handling intent', HTTP_BAD_REQUEST)
|
||||
|
||||
if action is not None:
|
||||
# API.AI expects a response in less than 5s
|
||||
if async_action:
|
||||
# Do not wait for the action to be executed.
|
||||
# Needed if the action will take longer than 5s to execute
|
||||
self.hass.async_add_job(action.async_run(response.parameters))
|
||||
else:
|
||||
# Wait for the action to be executed so we can use results to
|
||||
# render the answer
|
||||
yield from action.async_run(response.parameters)
|
||||
if 'plain' in intent_response.speech:
|
||||
apiai_response.add_speech(
|
||||
intent_response.speech['plain']['speech'])
|
||||
|
||||
# pylint: disable=unsubscriptable-object
|
||||
if speech is not None:
|
||||
response.add_speech(speech)
|
||||
|
||||
return self.json(response)
|
||||
return self.json(apiai_response)
|
||||
|
||||
|
||||
class ApiaiResponse(object):
|
||||
|
||||
253
homeassistant/components/apple_tv.py
Normal file
253
homeassistant/components/apple_tv.py
Normal file
@@ -0,0 +1,253 @@
|
||||
"""
|
||||
Support for Apple TV.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/apple_tv/
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_ENTITY_ID)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.components.discovery import SERVICE_APPLE_TV
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyatv==0.3.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'apple_tv'
|
||||
|
||||
SERVICE_SCAN = 'apple_tv_scan'
|
||||
SERVICE_AUTHENTICATE = 'apple_tv_authenticate'
|
||||
|
||||
ATTR_ATV = 'atv'
|
||||
ATTR_POWER = 'power'
|
||||
|
||||
CONF_LOGIN_ID = 'login_id'
|
||||
CONF_START_OFF = 'start_off'
|
||||
CONF_CREDENTIALS = 'credentials'
|
||||
|
||||
DEFAULT_NAME = 'Apple TV'
|
||||
|
||||
DATA_APPLE_TV = 'data_apple_tv'
|
||||
DATA_ENTITIES = 'data_apple_tv_entities'
|
||||
|
||||
KEY_CONFIG = 'apple_tv_configuring'
|
||||
|
||||
NOTIFICATION_AUTH_ID = 'apple_tv_auth_notification'
|
||||
NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication'
|
||||
NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification'
|
||||
NOTIFICATION_SCAN_TITLE = 'Apple TV Scan'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_LOGIN_ID): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_CREDENTIALS, default=None): cv.string,
|
||||
vol.Optional(CONF_START_OFF, default=False): cv.boolean
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
# Currently no attributes but it might change later
|
||||
APPLE_TV_SCAN_SCHEMA = vol.Schema({})
|
||||
|
||||
APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_ids,
|
||||
})
|
||||
|
||||
|
||||
def request_configuration(hass, config, atv, credentials):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = hass.components.configurator
|
||||
|
||||
@asyncio.coroutine
|
||||
def configuration_callback(callback_data):
|
||||
"""Handle the submitted configuration."""
|
||||
from pyatv import exceptions
|
||||
pin = callback_data.get('pin')
|
||||
|
||||
try:
|
||||
yield from atv.airplay.finish_authentication(pin)
|
||||
hass.components.persistent_notification.async_create(
|
||||
'Authentication succeeded!<br /><br />Add the following '
|
||||
'to credentials: in your apple_tv configuration:<br /><br />'
|
||||
'{0}'.format(credentials),
|
||||
title=NOTIFICATION_AUTH_TITLE,
|
||||
notification_id=NOTIFICATION_AUTH_ID)
|
||||
except exceptions.DeviceAuthenticationError as ex:
|
||||
hass.components.persistent_notification.async_create(
|
||||
'Authentication failed! Did you enter correct PIN?<br /><br />'
|
||||
'Details: {0}'.format(ex),
|
||||
title=NOTIFICATION_AUTH_TITLE,
|
||||
notification_id=NOTIFICATION_AUTH_ID)
|
||||
|
||||
hass.async_add_job(configurator.request_done, instance)
|
||||
|
||||
instance = configurator.request_config(
|
||||
hass, 'Apple TV Authentication', configuration_callback,
|
||||
description='Please enter PIN code shown on screen.',
|
||||
submit_caption='Confirm',
|
||||
fields=[{'id': 'pin', 'name': 'PIN Code', 'type': 'password'}]
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def scan_for_apple_tvs(hass):
|
||||
"""Scan for devices and present a notification of the ones found."""
|
||||
import pyatv
|
||||
atvs = yield from pyatv.scan_for_apple_tvs(hass.loop, timeout=3)
|
||||
|
||||
devices = []
|
||||
for atv in atvs:
|
||||
login_id = atv.login_id
|
||||
if login_id is None:
|
||||
login_id = 'Home Sharing disabled'
|
||||
devices.append('Name: {0}<br />Host: {1}<br />Login ID: {2}'.format(
|
||||
atv.name, atv.address, login_id))
|
||||
|
||||
if not devices:
|
||||
devices = ['No device(s) found']
|
||||
|
||||
hass.components.persistent_notification.async_create(
|
||||
'The following devices were found:<br /><br />' +
|
||||
'<br /><br />'.join(devices),
|
||||
title=NOTIFICATION_SCAN_TITLE,
|
||||
notification_id=NOTIFICATION_SCAN_ID)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up the Apple TV component."""
|
||||
if DATA_APPLE_TV not in hass.data:
|
||||
hass.data[DATA_APPLE_TV] = {}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_service_handler(service):
|
||||
"""Handler for service calls."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
if entity_ids:
|
||||
devices = [device for device in hass.data[DATA_ENTITIES]
|
||||
if device.entity_id in entity_ids]
|
||||
else:
|
||||
devices = hass.data[DATA_ENTITIES]
|
||||
|
||||
for device in devices:
|
||||
atv = device.atv
|
||||
if service.service == SERVICE_AUTHENTICATE:
|
||||
credentials = yield from atv.airplay.generate_credentials()
|
||||
yield from atv.airplay.load_credentials(credentials)
|
||||
_LOGGER.debug('Generated new credentials: %s', credentials)
|
||||
yield from atv.airplay.start_authentication()
|
||||
hass.async_add_job(request_configuration,
|
||||
hass, config, atv, credentials)
|
||||
elif service.service == SERVICE_SCAN:
|
||||
hass.async_add_job(scan_for_apple_tvs, hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def atv_discovered(service, info):
|
||||
"""Setup an Apple TV that was auto discovered."""
|
||||
yield from _setup_atv(hass, {
|
||||
CONF_NAME: info['name'],
|
||||
CONF_HOST: info['host'],
|
||||
CONF_LOGIN_ID: info['properties']['hG'],
|
||||
CONF_START_OFF: False
|
||||
})
|
||||
|
||||
discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered)
|
||||
|
||||
tasks = [_setup_atv(hass, conf) for conf in config.get(DOMAIN, [])]
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SCAN, async_service_handler,
|
||||
descriptions.get(SERVICE_SCAN),
|
||||
schema=APPLE_TV_SCAN_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_AUTHENTICATE, async_service_handler,
|
||||
descriptions.get(SERVICE_AUTHENTICATE),
|
||||
schema=APPLE_TV_AUTHENTICATE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _setup_atv(hass, atv_config):
|
||||
"""Setup an Apple TV."""
|
||||
import pyatv
|
||||
name = atv_config.get(CONF_NAME)
|
||||
host = atv_config.get(CONF_HOST)
|
||||
login_id = atv_config.get(CONF_LOGIN_ID)
|
||||
start_off = atv_config.get(CONF_START_OFF)
|
||||
credentials = atv_config.get(CONF_CREDENTIALS)
|
||||
|
||||
if host in hass.data[DATA_APPLE_TV]:
|
||||
return
|
||||
|
||||
details = pyatv.AppleTVDevice(name, host, login_id)
|
||||
session = async_get_clientsession(hass)
|
||||
atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session)
|
||||
if credentials:
|
||||
yield from atv.airplay.load_credentials(credentials)
|
||||
|
||||
power = AppleTVPowerManager(hass, atv, start_off)
|
||||
hass.data[DATA_APPLE_TV][host] = {
|
||||
ATTR_ATV: atv,
|
||||
ATTR_POWER: power
|
||||
}
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'media_player', DOMAIN, atv_config))
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'remote', DOMAIN, atv_config))
|
||||
|
||||
|
||||
class AppleTVPowerManager:
|
||||
"""Manager for global power management of an Apple TV.
|
||||
|
||||
An instance is used per device to share the same power state between
|
||||
several platforms.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, atv, is_off):
|
||||
"""Initialize power manager."""
|
||||
self.hass = hass
|
||||
self.atv = atv
|
||||
self.listeners = []
|
||||
self._is_on = not is_off
|
||||
|
||||
def init(self):
|
||||
"""Initialize power management."""
|
||||
if self._is_on:
|
||||
self.atv.push_updater.start()
|
||||
|
||||
@property
|
||||
def turned_on(self):
|
||||
"""If device is on or off."""
|
||||
return self._is_on
|
||||
|
||||
def set_power_on(self, value):
|
||||
"""Change if a device is on or off."""
|
||||
if value != self._is_on:
|
||||
self._is_on = value
|
||||
if not self._is_on:
|
||||
self.atv.push_updater.stop()
|
||||
else:
|
||||
self.atv.push_updater.start()
|
||||
|
||||
for listener in self.listeners:
|
||||
self.hass.async_add_job(listener.async_update_ha_state())
|
||||
@@ -1,27 +1,26 @@
|
||||
"""
|
||||
This component provides basic support for Netgear Arlo IP cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/arlo/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
import homeassistant.loader as loader
|
||||
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
|
||||
REQUIREMENTS = ['pyarlo==0.0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ATTRIBUTION = 'Data provided by arlo.netgear.com'
|
||||
|
||||
DOMAIN = 'arlo'
|
||||
CONF_ATTRIBUTION = "Data provided by arlo.netgear.com"
|
||||
|
||||
DATA_ARLO = 'data_arlo'
|
||||
DEFAULT_BRAND = 'Netgear Arlo'
|
||||
DOMAIN = 'arlo'
|
||||
|
||||
NOTIFICATION_ID = 'arlo_notification'
|
||||
NOTIFICATION_TITLE = 'Arlo Camera Setup'
|
||||
@@ -40,18 +39,17 @@ def setup(hass, config):
|
||||
username = conf.get(CONF_USERNAME)
|
||||
password = conf.get(CONF_PASSWORD)
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
try:
|
||||
from pyarlo import PyArlo
|
||||
|
||||
arlo = PyArlo(username, password, preload=False)
|
||||
if not arlo.is_connected:
|
||||
return False
|
||||
hass.data['arlo'] = arlo
|
||||
hass.data[DATA_ARLO] = arlo
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Netgar Arlo: %s", str(ex))
|
||||
persistent_notification.create(
|
||||
hass, 'Error: {}<br />'
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
|
||||
@@ -13,6 +13,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.setup import async_prepare_setup_platform
|
||||
from homeassistant.core import CoreState
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
@@ -105,6 +106,7 @@ TRIGGER_SERVICE_SCHEMA = vol.Schema({
|
||||
RELOAD_SERVICE_SCHEMA = vol.Schema({})
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass, entity_id):
|
||||
"""
|
||||
Return true if specified automation entity_id is on.
|
||||
@@ -114,35 +116,41 @@ def is_on(hass, entity_id):
|
||||
return hass.states.is_state(entity_id, STATE_ON)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def turn_on(hass, entity_id=None):
|
||||
"""Turn on specified automation or all."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def turn_off(hass, entity_id=None):
|
||||
"""Turn off specified automation or all."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def toggle(hass, entity_id=None):
|
||||
"""Toggle specified automation or all."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_TOGGLE, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def trigger(hass, entity_id=None):
|
||||
"""Trigger specified automation or all."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_TRIGGER, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def reload(hass):
|
||||
"""Reload the automation from config."""
|
||||
hass.services.call(DOMAIN, SERVICE_RELOAD)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def async_reload(hass):
|
||||
"""Reload the automation from config.
|
||||
|
||||
|
||||
@@ -12,13 +12,11 @@ import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import MATCH_ALL, CONF_PLATFORM
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_point_in_utc_time)
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
CONF_ENTITY_ID = 'entity_id'
|
||||
CONF_FROM = 'from'
|
||||
CONF_TO = 'to'
|
||||
CONF_STATE = 'state'
|
||||
CONF_FOR = 'for'
|
||||
|
||||
TRIGGER_SCHEMA = vol.All(
|
||||
@@ -28,11 +26,9 @@ TRIGGER_SCHEMA = vol.All(
|
||||
# These are str on purpose. Want to catch YAML conversions
|
||||
CONF_FROM: str,
|
||||
CONF_TO: str,
|
||||
CONF_STATE: str,
|
||||
CONF_FOR: vol.All(cv.time_period, cv.positive_timedelta),
|
||||
}),
|
||||
vol.Any(cv.key_dependency(CONF_FOR, CONF_TO),
|
||||
cv.key_dependency(CONF_FOR, CONF_STATE))
|
||||
cv.key_dependency(CONF_FOR, CONF_TO),
|
||||
)
|
||||
|
||||
|
||||
@@ -41,7 +37,7 @@ def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||
to_state = get_deprecated(config, CONF_TO, CONF_STATE, MATCH_ALL)
|
||||
to_state = config.get(CONF_TO, MATCH_ALL)
|
||||
time_delta = config.get(CONF_FOR)
|
||||
async_remove_state_for_cancel = None
|
||||
async_remove_state_for_listener = None
|
||||
|
||||
@@ -42,8 +42,6 @@ def async_trigger(hass, config, action):
|
||||
},
|
||||
})
|
||||
|
||||
# Do something to call action
|
||||
if event == SUN_EVENT_SUNRISE:
|
||||
return async_track_sunrise(hass, call_action, offset)
|
||||
else:
|
||||
return async_track_sunset(hass, call_action, offset)
|
||||
return async_track_sunset(hass, call_action, offset)
|
||||
|
||||
@@ -10,7 +10,7 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_AT, CONF_PLATFORM, CONF_AFTER
|
||||
from homeassistant.const import CONF_AT, CONF_PLATFORM
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_change
|
||||
|
||||
@@ -23,12 +23,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'time',
|
||||
CONF_AT: cv.time,
|
||||
CONF_AFTER: cv.time,
|
||||
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES,
|
||||
CONF_SECONDS, CONF_AT, CONF_AFTER))
|
||||
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -37,11 +35,6 @@ def async_trigger(hass, config, action):
|
||||
if CONF_AT in config:
|
||||
at_time = config.get(CONF_AT)
|
||||
hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second
|
||||
elif CONF_AFTER in config:
|
||||
_LOGGER.warning("'after' is deprecated for the time trigger. Please "
|
||||
"rename 'after' to 'at' in your configuration file.")
|
||||
at_time = config.get(CONF_AFTER)
|
||||
hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second
|
||||
else:
|
||||
hours = config.get(CONF_HOURS)
|
||||
minutes = config.get(CONF_MINUTES)
|
||||
|
||||
@@ -11,6 +11,7 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
|
||||
CONF_HOST, CONF_INCLUDE, CONF_NAME,
|
||||
CONF_PASSWORD, CONF_TRIGGER_TIME,
|
||||
@@ -18,11 +19,11 @@ from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
|
||||
from homeassistant.components.discovery import SERVICE_AXIS
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
|
||||
REQUIREMENTS = ['axis==7']
|
||||
REQUIREMENTS = ['axis==8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -59,10 +60,25 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SERVICE_VAPIX_CALL = 'vapix_call'
|
||||
SERVICE_VAPIX_CALL_RESPONSE = 'vapix_call_response'
|
||||
SERVICE_CGI = 'cgi'
|
||||
SERVICE_ACTION = 'action'
|
||||
SERVICE_PARAM = 'param'
|
||||
SERVICE_DEFAULT_CGI = 'param.cgi'
|
||||
SERVICE_DEFAULT_ACTION = 'update'
|
||||
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(SERVICE_PARAM): cv.string,
|
||||
vol.Optional(SERVICE_CGI, default=SERVICE_DEFAULT_CGI): cv.string,
|
||||
vol.Optional(SERVICE_ACTION, default=SERVICE_DEFAULT_ACTION): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def request_configuration(hass, name, host, serialnumber):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = get_component('configurator')
|
||||
configurator = hass.components.configurator
|
||||
|
||||
def configuration_callback(callback_data):
|
||||
"""Called when config is submitted."""
|
||||
@@ -135,23 +151,34 @@ def setup(hass, base_config):
|
||||
|
||||
def axis_device_discovered(service, discovery_info):
|
||||
"""Called when axis devices has been found."""
|
||||
host = discovery_info['host']
|
||||
host = discovery_info[CONF_HOST]
|
||||
name = discovery_info['hostname']
|
||||
serialnumber = discovery_info['properties']['macaddress']
|
||||
|
||||
if serialnumber not in AXIS_DEVICES:
|
||||
config_file = _read_config(hass)
|
||||
if serialnumber in config_file:
|
||||
# Device config saved to file
|
||||
try:
|
||||
config = DEVICE_SCHEMA(config_file[serialnumber])
|
||||
config[CONF_HOST] = host
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err)
|
||||
return False
|
||||
if not setup_device(hass, config):
|
||||
_LOGGER.error("Couldn\'t set up %s", config['name'])
|
||||
_LOGGER.error("Couldn\'t set up %s", config[CONF_NAME])
|
||||
else:
|
||||
# New device, create configuration request for UI
|
||||
request_configuration(hass, name, host, serialnumber)
|
||||
else:
|
||||
# Device already registered, but on a different IP
|
||||
device = AXIS_DEVICES[serialnumber]
|
||||
device.url = host
|
||||
async_dispatcher_send(hass,
|
||||
DOMAIN + '_' + device.name + '_new_ip',
|
||||
host)
|
||||
|
||||
# Register discovery service
|
||||
discovery.listen(hass, SERVICE_AXIS, axis_device_discovered)
|
||||
|
||||
if DOMAIN in base_config:
|
||||
@@ -160,7 +187,30 @@ def setup(hass, base_config):
|
||||
if CONF_NAME not in config:
|
||||
config[CONF_NAME] = device
|
||||
if not setup_device(hass, config):
|
||||
_LOGGER.error("Couldn\'t set up %s", config['name'])
|
||||
_LOGGER.error("Couldn\'t set up %s", config[CONF_NAME])
|
||||
|
||||
# Services to communicate with device.
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def vapix_service(call):
|
||||
"""Service to send a message."""
|
||||
for _, device in AXIS_DEVICES.items():
|
||||
if device.name == call.data[CONF_NAME]:
|
||||
response = device.do_request(call.data[SERVICE_CGI],
|
||||
call.data[SERVICE_ACTION],
|
||||
call.data[SERVICE_PARAM])
|
||||
hass.bus.async_fire(SERVICE_VAPIX_CALL_RESPONSE, response)
|
||||
return True
|
||||
_LOGGER.info("Couldn\'t find device %s", call.data[CONF_NAME])
|
||||
return False
|
||||
|
||||
# Register service with Home Assistant.
|
||||
hass.services.register(DOMAIN,
|
||||
SERVICE_VAPIX_CALL,
|
||||
vapix_service,
|
||||
descriptions[DOMAIN][SERVICE_VAPIX_CALL],
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
@@ -190,8 +240,15 @@ def setup_device(hass, config):
|
||||
|
||||
if enable_metadatastream:
|
||||
device.initialize_new_event = event_initialized
|
||||
device.initiate_metadatastream()
|
||||
if not device.initiate_metadatastream():
|
||||
hass.components.persistent_notification.create(
|
||||
'Dependency missing for sensors, '
|
||||
'please check documentation',
|
||||
title=DOMAIN,
|
||||
notification_id='axis_notification')
|
||||
|
||||
AXIS_DEVICES[device.serial_number] = device
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -311,4 +368,4 @@ REMAP = [{'type': 'motion',
|
||||
'class': 'input',
|
||||
'topic': 'tns1:Device/tnsaxis:IO/Port',
|
||||
'subscribe': 'onvif:Device/axis:IO/Port',
|
||||
'platform': 'sensor'}, ]
|
||||
'platform': 'binary_sensor'}, ]
|
||||
|
||||
@@ -51,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
add_devices([ArestBinarySensor(
|
||||
arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
|
||||
device_class, pin)])
|
||||
device_class, pin)], True)
|
||||
|
||||
|
||||
class ArestBinarySensor(BinarySensorDevice):
|
||||
@@ -64,12 +64,11 @@ class ArestBinarySensor(BinarySensorDevice):
|
||||
self._name = name
|
||||
self._device_class = device_class
|
||||
self._pin = pin
|
||||
self.update()
|
||||
|
||||
if self._pin is not None:
|
||||
request = requests.get(
|
||||
'{}/mode/{}/i'.format(self._resource, self._pin), timeout=10)
|
||||
if request.status_code is not 200:
|
||||
if request.status_code != 200:
|
||||
_LOGGER.error("Can't set mode of %s", self._resource)
|
||||
|
||||
@property
|
||||
|
||||
@@ -8,19 +8,18 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.digital_ocean import (
|
||||
CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME,
|
||||
ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY,
|
||||
ATTR_REGION, ATTR_VCPUS)
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
ATTR_REGION, ATTR_VCPUS, DATA_DIGITAL_OCEAN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Droplet'
|
||||
DEFAULT_SENSOR_CLASS = 'motion'
|
||||
DEFAULT_SENSOR_CLASS = 'moving'
|
||||
DEPENDENCIES = ['digital_ocean']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -30,19 +29,21 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Digital Ocean droplet sensor."""
|
||||
digital_ocean = get_component('digital_ocean')
|
||||
digital = hass.data.get(DATA_DIGITAL_OCEAN)
|
||||
if not digital:
|
||||
return False
|
||||
|
||||
droplets = config.get(CONF_DROPLETS)
|
||||
|
||||
dev = []
|
||||
for droplet in droplets:
|
||||
droplet_id = digital_ocean.DIGITAL_OCEAN.get_droplet_id(droplet)
|
||||
droplet_id = digital.get_droplet_id(droplet)
|
||||
if droplet_id is None:
|
||||
_LOGGER.error("Droplet %s is not available", droplet)
|
||||
return False
|
||||
dev.append(DigitalOceanBinarySensor(
|
||||
digital_ocean.DIGITAL_OCEAN, droplet_id))
|
||||
dev.append(DigitalOceanBinarySensor(digital, droplet_id))
|
||||
|
||||
add_devices(dev)
|
||||
add_devices(dev, True)
|
||||
|
||||
|
||||
class DigitalOceanBinarySensor(BinarySensorDevice):
|
||||
@@ -53,7 +54,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
|
||||
self._digital_ocean = do
|
||||
self._droplet_id = droplet_id
|
||||
self._state = None
|
||||
self.update()
|
||||
self.data = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -199,11 +199,10 @@ class FlicButton(BinarySensorDevice):
|
||||
"Queued %s dropped for %s. Time in queue was %s",
|
||||
click_type, self.address, time_string)
|
||||
return True
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Queued %s allowed for %s. Time in queue was %s",
|
||||
click_type, self.address, time_string)
|
||||
return False
|
||||
_LOGGER.info(
|
||||
"Queued %s allowed for %s. Time in queue was %s",
|
||||
click_type, self.address, time_string)
|
||||
return False
|
||||
|
||||
def _on_up_down(self, channel, click_type, was_queued, time_diff):
|
||||
"""Update device state, if event was not queued."""
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
|
||||
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
|
||||
REQUIREMENTS = ['pyhik==0.1.2']
|
||||
REQUIREMENTS = ['pyhik==0.1.3']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IGNORED = 'ignored'
|
||||
|
||||
@@ -34,8 +34,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMBinarySensor(hass, config)
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMBinarySensor(hass, conf)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.modbus as modbus
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.const import CONF_NAME, CONF_SLAVE
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
@@ -18,7 +18,6 @@ DEPENDENCIES = ['modbus']
|
||||
|
||||
CONF_COIL = 'coil'
|
||||
CONF_COILS = 'coils'
|
||||
CONF_SLAVE = 'slave'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COILS): [{
|
||||
@@ -50,6 +49,11 @@ class ModbusCoilSensor(BinarySensorDevice):
|
||||
self._coil = int(coil)
|
||||
self._value = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the sensor."""
|
||||
@@ -58,4 +62,10 @@ class ModbusCoilSensor(BinarySensorDevice):
|
||||
def update(self):
|
||||
"""Update the state of the sensor."""
|
||||
result = modbus.HUB.read_coils(self._slave, self._coil, 1)
|
||||
self._value = result.bits[0]
|
||||
try:
|
||||
self._value = result.bits[0]
|
||||
except AttributeError:
|
||||
_LOGGER.error(
|
||||
'No response from modbus slave %s coil %s',
|
||||
self._slave,
|
||||
self._coil)
|
||||
|
||||
@@ -156,8 +156,7 @@ class NetatmoBinarySensor(BinarySensorDevice):
|
||||
return WELCOME_SENSOR_TYPES.get(self._sensor_name)
|
||||
elif self._cameratype == 'NOC':
|
||||
return PRESENCE_SENSOR_TYPES.get(self._sensor_name)
|
||||
else:
|
||||
return TAG_SENSOR_TYPES.get(self._sensor_name)
|
||||
return TAG_SENSOR_TYPES.get(self._sensor_name)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
||||
@@ -12,13 +12,12 @@ import voluptuous as vol
|
||||
from homeassistant.const import CONF_NAME, CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['octoprint']
|
||||
|
||||
DOMAIN = "octoprint"
|
||||
DEFAULT_NAME = 'OctoPrint'
|
||||
|
||||
SENSOR_TYPES = {
|
||||
@@ -37,7 +36,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the available OctoPrint binary sensors."""
|
||||
octoprint = get_component('octoprint')
|
||||
octoprint_api = hass.data[DOMAIN]["api"]
|
||||
name = config.get(CONF_NAME)
|
||||
monitored_conditions = config.get(
|
||||
CONF_MONITORED_CONDITIONS, SENSOR_TYPES.keys())
|
||||
@@ -45,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
devices = []
|
||||
for octo_type in monitored_conditions:
|
||||
new_sensor = OctoPrintBinarySensor(
|
||||
octoprint.OCTOPRINT, octo_type, SENSOR_TYPES[octo_type][2],
|
||||
octoprint_api, octo_type, SENSOR_TYPES[octo_type][2],
|
||||
name, SENSOR_TYPES[octo_type][3], SENSOR_TYPES[octo_type][0],
|
||||
SENSOR_TYPES[octo_type][1], 'flags')
|
||||
devices.append(new_sensor)
|
||||
@@ -98,6 +97,3 @@ class OctoPrintBinarySensor(BinarySensorDevice):
|
||||
except requests.exceptions.ConnectionError:
|
||||
# Error calling the api, already logged in api.update()
|
||||
return
|
||||
|
||||
if self._state is None:
|
||||
_LOGGER.warning("Unable to locate value for %s", self.sensor_type)
|
||||
|
||||
@@ -28,6 +28,7 @@ from homeassistant.util import dt as dt_util
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_VARIABLE = 'variable'
|
||||
CONF_RESET_DELAY_SEC = 'reset_delay_sec'
|
||||
|
||||
DEFAULT_NAME = 'Pilight Binary Sensor'
|
||||
DEPENDENCIES = ['pilight']
|
||||
@@ -38,7 +39,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
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_DISARM_AFTER_TRIGGER, default=False): cv.boolean
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER, default=False): cv.boolean,
|
||||
vol.Optional(CONF_RESET_DELAY_SEC, default=30): cv.positive_int
|
||||
})
|
||||
|
||||
|
||||
@@ -54,6 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
payload=config.get(CONF_PAYLOAD),
|
||||
on_value=config.get(CONF_PAYLOAD_ON),
|
||||
off_value=config.get(CONF_PAYLOAD_OFF),
|
||||
rst_dly_sec=config.get(CONF_RESET_DELAY_SEC),
|
||||
)])
|
||||
else:
|
||||
add_devices([PilightBinarySensor(
|
||||
|
||||
@@ -35,6 +35,9 @@ SCAN_INTERVAL = timedelta(minutes=5)
|
||||
PING_MATCHER = re.compile(
|
||||
r'(?P<min>\d+.\d+)\/(?P<avg>\d+.\d+)\/(?P<max>\d+.\d+)\/(?P<mdev>\d+.\d+)')
|
||||
|
||||
PING_MATCHER_BUSYBOX = re.compile(
|
||||
r'(?P<min>\d+.\d+)\/(?P<avg>\d+.\d+)\/(?P<max>\d+.\d+)')
|
||||
|
||||
WIN32_PING_MATCHER = re.compile(
|
||||
r'(?P<min>\d+)ms.+(?P<max>\d+)ms.+(?P<avg>\d+)ms')
|
||||
|
||||
@@ -126,14 +129,21 @@ class PingData(object):
|
||||
'avg': rtt_avg,
|
||||
'max': rtt_max,
|
||||
'mdev': ''}
|
||||
else:
|
||||
match = PING_MATCHER.search(str(out).split('\n')[-1])
|
||||
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
|
||||
if 'max/' not in str(out):
|
||||
match = PING_MATCHER_BUSYBOX.search(str(out).split('\n')[-1])
|
||||
rtt_min, rtt_avg, rtt_max = match.groups()
|
||||
return {
|
||||
'min': rtt_min,
|
||||
'avg': rtt_avg,
|
||||
'max': rtt_max,
|
||||
'mdev': rtt_mdev}
|
||||
'mdev': ''}
|
||||
match = PING_MATCHER.search(str(out).split('\n')[-1])
|
||||
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
|
||||
return {
|
||||
'min': rtt_min,
|
||||
'avg': rtt_avg,
|
||||
'max': rtt_max,
|
||||
'mdev': rtt_mdev}
|
||||
except (subprocess.CalledProcessError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
@@ -107,6 +107,8 @@ class RestBinarySensor(BinarySensorDevice):
|
||||
if self.rest.data is None:
|
||||
return False
|
||||
|
||||
response = self.rest.data
|
||||
|
||||
if self._value_template is not None:
|
||||
response = self._value_template.\
|
||||
async_render_with_possible_json_value(self.rest.data, False)
|
||||
|
||||
232
homeassistant/components/binary_sensor/rfxtrx.py
Normal file
232
homeassistant/components/binary_sensor/rfxtrx.py
Normal file
@@ -0,0 +1,232 @@
|
||||
"""
|
||||
Support for RFXtrx binary sensors.
|
||||
|
||||
Lighting4 devices (sensors based on PT2262 encoder) are supported and
|
||||
tested. Other types may need some work.
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
from homeassistant.components import rfxtrx
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import event as evt
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.rfxtrx import (
|
||||
ATTR_AUTOMATIC_ADD, ATTR_NAME, ATTR_OFF_DELAY, ATTR_FIREEVENT,
|
||||
ATTR_DATA_BITS, CONF_DEVICES
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["rfxtrx"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required("platform"): rfxtrx.DOMAIN,
|
||||
vol.Optional(CONF_DEVICES, default={}): vol.All(
|
||||
dict, rfxtrx.valid_binary_sensor),
|
||||
vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup the Binary Sensor platform to rfxtrx."""
|
||||
import RFXtrx as rfxtrxmod
|
||||
sensors = []
|
||||
|
||||
for packet_id, entity in config['devices'].items():
|
||||
event = rfxtrx.get_rfx_object(packet_id)
|
||||
device_id = slugify(event.device.id_string.lower())
|
||||
|
||||
if device_id in rfxtrx.RFX_DEVICES:
|
||||
continue
|
||||
|
||||
if entity[ATTR_DATA_BITS] is not None:
|
||||
_LOGGER.info("Masked device id: %s",
|
||||
rfxtrx.get_pt2262_deviceid(device_id,
|
||||
entity[ATTR_DATA_BITS]))
|
||||
|
||||
_LOGGER.info("Add %s rfxtrx.binary_sensor (class %s)",
|
||||
entity[ATTR_NAME], entity[CONF_DEVICE_CLASS])
|
||||
|
||||
device = RfxtrxBinarySensor(event, entity[ATTR_NAME],
|
||||
entity[CONF_DEVICE_CLASS],
|
||||
entity[ATTR_FIREEVENT],
|
||||
entity[ATTR_OFF_DELAY],
|
||||
entity[ATTR_DATA_BITS],
|
||||
entity[CONF_COMMAND_ON],
|
||||
entity[CONF_COMMAND_OFF])
|
||||
device.hass = hass
|
||||
device.is_lighting4 = (packet_id[2:4] == '13')
|
||||
sensors.append(device)
|
||||
rfxtrx.RFX_DEVICES[device_id] = device
|
||||
|
||||
add_devices_callback(sensors)
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def binary_sensor_update(event):
|
||||
"""Callback for control updates from the RFXtrx gateway."""
|
||||
if not isinstance(event, rfxtrxmod.ControlEvent):
|
||||
return
|
||||
|
||||
device_id = slugify(event.device.id_string.lower())
|
||||
|
||||
if device_id in rfxtrx.RFX_DEVICES:
|
||||
sensor = rfxtrx.RFX_DEVICES[device_id]
|
||||
else:
|
||||
sensor = rfxtrx.get_pt2262_device(device_id)
|
||||
|
||||
if sensor is None:
|
||||
# Add the entity if not exists and automatic_add is True
|
||||
if not config[ATTR_AUTOMATIC_ADD]:
|
||||
return
|
||||
|
||||
poss_dev = rfxtrx.find_possible_pt2262_device(device_id)
|
||||
|
||||
if poss_dev is not None:
|
||||
poss_id = slugify(poss_dev.event.device.id_string.lower())
|
||||
_LOGGER.info("Found possible matching deviceid %s.",
|
||||
poss_id)
|
||||
|
||||
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
|
||||
sensor = RfxtrxBinarySensor(event, pkt_id)
|
||||
sensor.hass = hass
|
||||
sensor.is_lighting4 = (pkt_id[2:4] == '13')
|
||||
rfxtrx.RFX_DEVICES[device_id] = sensor
|
||||
add_devices_callback([sensor])
|
||||
_LOGGER.info("Added binary sensor %s "
|
||||
"(Device_id: %s Class: %s Sub: %s)",
|
||||
pkt_id,
|
||||
slugify(event.device.id_string.lower()),
|
||||
event.device.__class__.__name__,
|
||||
event.device.subtype)
|
||||
|
||||
elif not isinstance(sensor, RfxtrxBinarySensor):
|
||||
return
|
||||
else:
|
||||
_LOGGER.info("Binary sensor update "
|
||||
"(Device_id: %s Class: %s Sub: %s)",
|
||||
slugify(event.device.id_string.lower()),
|
||||
event.device.__class__.__name__,
|
||||
event.device.subtype)
|
||||
if sensor.is_lighting4:
|
||||
if sensor.data_bits is not None:
|
||||
cmd = rfxtrx.get_pt2262_cmd(device_id, sensor.data_bits)
|
||||
sensor.apply_cmd(int(cmd, 16))
|
||||
else:
|
||||
sensor.update_state(True)
|
||||
else:
|
||||
rfxtrx.apply_received_command(event)
|
||||
|
||||
if (sensor.is_on and sensor.off_delay is not None and
|
||||
sensor.delay_listener is None):
|
||||
|
||||
def off_delay_listener(now):
|
||||
"""Switch device off after a delay."""
|
||||
sensor.delay_listener = None
|
||||
sensor.update_state(False)
|
||||
|
||||
sensor.delay_listener = evt.track_point_in_time(
|
||||
hass, off_delay_listener, dt_util.utcnow() + sensor.off_delay
|
||||
)
|
||||
|
||||
# Subscribe to main rfxtrx events
|
||||
if binary_sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
|
||||
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(binary_sensor_update)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes,too-many-arguments
|
||||
class RfxtrxBinarySensor(BinarySensorDevice):
|
||||
"""An Rfxtrx binary sensor."""
|
||||
|
||||
def __init__(self, event, name, device_class=None,
|
||||
should_fire=False, off_delay=None, data_bits=None,
|
||||
cmd_on=None, cmd_off=None):
|
||||
"""Initialize the sensor."""
|
||||
self.event = event
|
||||
self._name = name
|
||||
self._should_fire_event = should_fire
|
||||
self._device_class = device_class
|
||||
self._off_delay = off_delay
|
||||
self._state = False
|
||||
self.is_lighting4 = False
|
||||
self.delay_listener = None
|
||||
self._data_bits = data_bits
|
||||
self._cmd_on = cmd_on
|
||||
self._cmd_off = cmd_off
|
||||
|
||||
if data_bits is not None:
|
||||
self._masked_id = rfxtrx.get_pt2262_deviceid(
|
||||
event.device.id_string.lower(),
|
||||
data_bits)
|
||||
|
||||
def __str__(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the device name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def masked_id(self):
|
||||
"""Return the masked device id (isolated address bits)."""
|
||||
return self._masked_id
|
||||
|
||||
@property
|
||||
def data_bits(self):
|
||||
"""Return the number of data bits."""
|
||||
return self._data_bits
|
||||
|
||||
@property
|
||||
def cmd_on(self):
|
||||
"""Return the value of the 'On' command."""
|
||||
return self._cmd_on
|
||||
|
||||
@property
|
||||
def cmd_off(self):
|
||||
"""Return the value of the 'Off' command."""
|
||||
return self._cmd_off
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def should_fire_event(self):
|
||||
"""Return is the device must fire event."""
|
||||
return self._should_fire_event
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the sensor class."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def off_delay(self):
|
||||
"""Return the off_delay attribute value."""
|
||||
return self._off_delay
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the sensor state is True."""
|
||||
return self._state
|
||||
|
||||
def apply_cmd(self, cmd):
|
||||
"""Apply a command for updating the state."""
|
||||
if cmd == self.cmd_on:
|
||||
self.update_state(True)
|
||||
elif cmd == self.cmd_off:
|
||||
self.update_state(False)
|
||||
|
||||
def update_state(self, state):
|
||||
"""Update the state of the device."""
|
||||
self._state = state
|
||||
self.schedule_update_ha_state()
|
||||
96
homeassistant/components/binary_sensor/velbus.py
Normal file
96
homeassistant/components/binary_sensor/velbus.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Support for Velbus Binary Sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.velbus/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_NAME, CONF_DEVICES
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.components.velbus import DOMAIN
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
DEPENDENCIES = ['velbus']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [
|
||||
{
|
||||
vol.Required('module'): cv.positive_int,
|
||||
vol.Required('channel'): cv.positive_int,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional('is_pushbutton'): cv.boolean
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Velbus binary sensors."""
|
||||
velbus = hass.data[DOMAIN]
|
||||
|
||||
add_devices(VelbusBinarySensor(sensor, velbus)
|
||||
for sensor in config[CONF_DEVICES])
|
||||
|
||||
|
||||
class VelbusBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Velbus Binary Sensor."""
|
||||
|
||||
def __init__(self, binary_sensor, velbus):
|
||||
"""Initialize a Velbus light."""
|
||||
self._velbus = velbus
|
||||
self._name = binary_sensor[CONF_NAME]
|
||||
self._module = binary_sensor['module']
|
||||
self._channel = binary_sensor['channel']
|
||||
self._is_pushbutton = 'is_pushbutton' in binary_sensor \
|
||||
and binary_sensor['is_pushbutton']
|
||||
self._state = False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Add listener for Velbus messages on bus."""
|
||||
yield from self.hass.async_add_job(
|
||||
self._velbus.subscribe, self._on_message)
|
||||
|
||||
def _on_message(self, message):
|
||||
import velbus
|
||||
if isinstance(message, velbus.PushButtonStatusMessage):
|
||||
if message.address == self._module and \
|
||||
self._channel in message.get_channels():
|
||||
if self._is_pushbutton:
|
||||
if self._channel in message.closed:
|
||||
self._toggle()
|
||||
else:
|
||||
pass
|
||||
else:
|
||||
self._toggle()
|
||||
|
||||
def _toggle(self):
|
||||
if self._state is True:
|
||||
self._state = False
|
||||
else:
|
||||
self._state = True
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the display name of this sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the sensor is on."""
|
||||
return self._state
|
||||
59
homeassistant/components/binary_sensor/verisure.py
Normal file
59
homeassistant/components/binary_sensor/verisure.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Interfaces with Verisure sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.verisure/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.verisure import HUB as hub
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.verisure import CONF_DOOR_WINDOW
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Verisure binary sensors."""
|
||||
sensors = []
|
||||
hub.update_overview()
|
||||
|
||||
if int(hub.config.get(CONF_DOOR_WINDOW, 1)):
|
||||
sensors.extend([
|
||||
VerisureDoorWindowSensor(device_label)
|
||||
for device_label in hub.get(
|
||||
"$.doorWindow.doorWindowDevice[*].deviceLabel")])
|
||||
add_devices(sensors)
|
||||
|
||||
|
||||
class VerisureDoorWindowSensor(BinarySensorDevice):
|
||||
"""Verisure door window sensor."""
|
||||
|
||||
def __init__(self, device_label):
|
||||
"""Initialize the modbus coil sensor."""
|
||||
self._device_label = device_label
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return hub.get_first(
|
||||
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].area",
|
||||
self._device_label)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the sensor."""
|
||||
return hub.get_first(
|
||||
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].state",
|
||||
self._device_label) == "OPEN"
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return hub.get_first(
|
||||
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')]",
|
||||
self._device_label) is not None
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the sensor."""
|
||||
hub.update_overview()
|
||||
@@ -30,8 +30,7 @@ class VolvoSensor(VolvoEntity, BinarySensorDevice):
|
||||
return bool(val)
|
||||
elif self._attribute in ['doors', 'windows']:
|
||||
return any([val[key] for key in val if 'Open' in key])
|
||||
else:
|
||||
return val != 'Normal'
|
||||
return val != 'Normal'
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
||||
@@ -121,10 +121,6 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
class WinkSmokeDetector(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Smoke detector."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
@@ -136,10 +132,6 @@ class WinkSmokeDetector(WinkBinarySensorDevice):
|
||||
class WinkHub(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Hub."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
@@ -152,10 +144,6 @@ class WinkHub(WinkBinarySensorDevice):
|
||||
class WinkRemote(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Lutron Connected bulb remote."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
@@ -175,10 +163,6 @@ class WinkRemote(WinkBinarySensorDevice):
|
||||
class WinkButton(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Relay button."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
@@ -191,10 +175,6 @@ class WinkButton(WinkBinarySensorDevice):
|
||||
class WinkGang(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Relay gang."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the gang is connected."""
|
||||
|
||||
316
homeassistant/components/binary_sensor/xiaomi.py
Normal file
316
homeassistant/components/binary_sensor/xiaomi.py
Normal file
@@ -0,0 +1,316 @@
|
||||
"""Support for Xiaomi binary sensors."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.xiaomi import (PY_XIAOMI_GATEWAY, XiaomiDevice)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NO_CLOSE = 'no_close'
|
||||
ATTR_OPEN_SINCE = 'Open since'
|
||||
|
||||
MOTION = 'motion'
|
||||
NO_MOTION = 'no_motion'
|
||||
ATTR_NO_MOTION_SINCE = 'No motion since'
|
||||
|
||||
DENSITY = 'density'
|
||||
ATTR_DENSITY = 'Density'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Perform the setup for Xiaomi devices."""
|
||||
devices = []
|
||||
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items():
|
||||
for device in gateway.devices['binary_sensor']:
|
||||
model = device['model']
|
||||
if model == 'motion':
|
||||
devices.append(XiaomiMotionSensor(device, hass, gateway))
|
||||
elif model == 'sensor_motion.aq2':
|
||||
devices.append(XiaomiMotionSensor(device, hass, gateway))
|
||||
elif model == 'magnet':
|
||||
devices.append(XiaomiDoorSensor(device, gateway))
|
||||
elif model == 'sensor_magnet.aq2':
|
||||
devices.append(XiaomiDoorSensor(device, gateway))
|
||||
elif model == 'smoke':
|
||||
devices.append(XiaomiSmokeSensor(device, gateway))
|
||||
elif model == 'natgas':
|
||||
devices.append(XiaomiNatgasSensor(device, gateway))
|
||||
elif model == 'switch':
|
||||
devices.append(XiaomiButton(device, 'Switch', 'status',
|
||||
hass, gateway))
|
||||
elif model == 'sensor_switch.aq2':
|
||||
devices.append(XiaomiButton(device, 'Switch', 'status',
|
||||
hass, gateway))
|
||||
elif model == '86sw1':
|
||||
devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0',
|
||||
hass, gateway))
|
||||
elif model == '86sw2':
|
||||
devices.append(XiaomiButton(device, 'Wall Switch (Left)',
|
||||
'channel_0', hass, gateway))
|
||||
devices.append(XiaomiButton(device, 'Wall Switch (Right)',
|
||||
'channel_1', hass, gateway))
|
||||
devices.append(XiaomiButton(device, 'Wall Switch (Both)',
|
||||
'dual_channel', hass, gateway))
|
||||
elif model == 'cube':
|
||||
devices.append(XiaomiCube(device, hass, gateway))
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class XiaomiBinarySensor(XiaomiDevice, BinarySensorDevice):
|
||||
"""Representation of a base XiaomiBinarySensor."""
|
||||
|
||||
def __init__(self, device, name, xiaomi_hub, data_key, device_class):
|
||||
"""Initialize the XiaomiSmokeSensor."""
|
||||
self._data_key = data_key
|
||||
self._device_class = device_class
|
||||
self._should_poll = False
|
||||
self._density = 0
|
||||
XiaomiDevice.__init__(self, device, name, xiaomi_hub)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return True if entity has to be polled for state."""
|
||||
return self._should_poll
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of binary sensor."""
|
||||
return self._device_class
|
||||
|
||||
def update(self):
|
||||
"""Update the sensor state."""
|
||||
_LOGGER.debug('Updating xiaomi sensor by polling')
|
||||
self._get_from_hub(self._sid)
|
||||
|
||||
|
||||
class XiaomiNatgasSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiNatgasSensor."""
|
||||
|
||||
def __init__(self, device, xiaomi_hub):
|
||||
"""Initialize the XiaomiSmokeSensor."""
|
||||
self._density = None
|
||||
XiaomiBinarySensor.__init__(self, device, 'Natgas Sensor', xiaomi_hub,
|
||||
'alarm', 'gas')
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {ATTR_DENSITY: self._density}
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
if DENSITY in data:
|
||||
self._density = int(data.get(DENSITY))
|
||||
|
||||
value = data.get(self._data_key)
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
if value == '1':
|
||||
if self._state:
|
||||
return False
|
||||
self._state = True
|
||||
return True
|
||||
elif value == '0':
|
||||
if self._state:
|
||||
self._state = False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class XiaomiMotionSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiMotionSensor."""
|
||||
|
||||
def __init__(self, device, hass, xiaomi_hub):
|
||||
"""Initialize the XiaomiMotionSensor."""
|
||||
self._hass = hass
|
||||
self._no_motion_since = 0
|
||||
XiaomiBinarySensor.__init__(self, device, 'Motion Sensor', xiaomi_hub,
|
||||
'status', 'motion')
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {ATTR_NO_MOTION_SINCE: self._no_motion_since}
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
self._should_poll = False
|
||||
if NO_MOTION in data: # handle push from the hub
|
||||
self._no_motion_since = data[NO_MOTION]
|
||||
self._state = False
|
||||
return True
|
||||
|
||||
value = data.get(self._data_key)
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
if value == MOTION:
|
||||
self._should_poll = True
|
||||
if self.entity_id is not None:
|
||||
self._hass.bus.fire('motion', {
|
||||
'entity_id': self.entity_id
|
||||
})
|
||||
|
||||
self._no_motion_since = 0
|
||||
if self._state:
|
||||
return False
|
||||
self._state = True
|
||||
return True
|
||||
elif value == NO_MOTION:
|
||||
if not self._state:
|
||||
return False
|
||||
self._state = False
|
||||
return True
|
||||
|
||||
|
||||
class XiaomiDoorSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiDoorSensor."""
|
||||
|
||||
def __init__(self, device, xiaomi_hub):
|
||||
"""Initialize the XiaomiDoorSensor."""
|
||||
self._open_since = 0
|
||||
XiaomiBinarySensor.__init__(self, device, 'Door Window Sensor',
|
||||
xiaomi_hub, 'status', 'opening')
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {ATTR_OPEN_SINCE: self._open_since}
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
self._should_poll = False
|
||||
if NO_CLOSE in data: # handle push from the hub
|
||||
self._open_since = data[NO_CLOSE]
|
||||
return True
|
||||
|
||||
value = data.get(self._data_key)
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
if value == 'open':
|
||||
self._should_poll = True
|
||||
if self._state:
|
||||
return False
|
||||
self._state = True
|
||||
return True
|
||||
elif value == 'close':
|
||||
self._open_since = 0
|
||||
if self._state:
|
||||
self._state = False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class XiaomiSmokeSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiSmokeSensor."""
|
||||
|
||||
def __init__(self, device, xiaomi_hub):
|
||||
"""Initialize the XiaomiSmokeSensor."""
|
||||
self._density = 0
|
||||
XiaomiBinarySensor.__init__(self, device, 'Smoke Sensor', xiaomi_hub,
|
||||
'alarm', 'smoke')
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {ATTR_DENSITY: self._density}
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
if DENSITY in data:
|
||||
self._density = int(data.get(DENSITY))
|
||||
value = data.get(self._data_key)
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
if value == '1':
|
||||
if self._state:
|
||||
return False
|
||||
self._state = True
|
||||
return True
|
||||
elif value == '0':
|
||||
if self._state:
|
||||
self._state = False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class XiaomiButton(XiaomiBinarySensor):
|
||||
"""Representation of a Xiaomi Button."""
|
||||
|
||||
def __init__(self, device, name, data_key, hass, xiaomi_hub):
|
||||
"""Initialize the XiaomiButton."""
|
||||
self._hass = hass
|
||||
XiaomiBinarySensor.__init__(self, device, name, xiaomi_hub,
|
||||
data_key, None)
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
value = data.get(self._data_key)
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
if value == 'long_click_press':
|
||||
self._state = True
|
||||
click_type = 'long_click_press'
|
||||
elif value == 'long_click_release':
|
||||
self._state = False
|
||||
click_type = 'hold'
|
||||
elif value == 'click':
|
||||
click_type = 'single'
|
||||
elif value == 'double_click':
|
||||
click_type = 'double'
|
||||
elif value == 'both_click':
|
||||
click_type = 'both'
|
||||
else:
|
||||
return False
|
||||
|
||||
self._hass.bus.fire('click', {
|
||||
'entity_id': self.entity_id,
|
||||
'click_type': click_type
|
||||
})
|
||||
if value in ['long_click_press', 'long_click_release']:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class XiaomiCube(XiaomiBinarySensor):
|
||||
"""Representation of a Xiaomi Cube."""
|
||||
|
||||
def __init__(self, device, hass, xiaomi_hub):
|
||||
"""Initialize the Xiaomi Cube."""
|
||||
self._hass = hass
|
||||
self._state = False
|
||||
XiaomiBinarySensor.__init__(self, device, 'Cube', xiaomi_hub,
|
||||
None, None)
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
if 'status' in data:
|
||||
self._hass.bus.fire('cube_action', {
|
||||
'entity_id': self.entity_id,
|
||||
'action_type': data['status']
|
||||
})
|
||||
|
||||
if 'rotate' in data:
|
||||
self._hass.bus.fire('cube_action', {
|
||||
'entity_id': self.entity_id,
|
||||
'action_type': 'rotate',
|
||||
'action_value': float(data['rotate'].replace(",", "."))
|
||||
})
|
||||
return False
|
||||
@@ -34,10 +34,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
from bellows.zigbee.zcl.clusters.security import IasZone
|
||||
|
||||
clusters = discovery_info['clusters']
|
||||
in_clusters = discovery_info['in_clusters']
|
||||
|
||||
device_class = None
|
||||
cluster = [c for c in clusters if isinstance(c, IasZone)][0]
|
||||
cluster = in_clusters[IasZone.cluster_id]
|
||||
if discovery_info['new_join']:
|
||||
yield from cluster.bind()
|
||||
ieee = cluster.endpoint.device.application.ieee
|
||||
@@ -64,7 +64,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
super().__init__(**kwargs)
|
||||
self._device_class = device_class
|
||||
from bellows.zigbee.zcl.clusters.security import IasZone
|
||||
self._ias_zone_cluster = self._clusters[IasZone.cluster_id]
|
||||
self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id]
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
|
||||
@@ -148,8 +148,7 @@ class CalendarEventDevice(Entity):
|
||||
if 'date' in date:
|
||||
return dt.start_of_local_day(dt.dt.datetime.combine(
|
||||
dt.parse_date(date['date']), dt.dt.time.min))
|
||||
else:
|
||||
return dt.as_local(dt.parse_datetime(date['dateTime']))
|
||||
return dt.as_local(dt.parse_datetime(date['dateTime']))
|
||||
|
||||
start = _get_date(self.data.event['start'])
|
||||
end = _get_date(self.data.event['end'])
|
||||
|
||||
@@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices, disc_info=None):
|
||||
if disc_info is None:
|
||||
return
|
||||
|
||||
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
|
||||
if not any(data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]):
|
||||
return
|
||||
|
||||
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
|
||||
|
||||
@@ -12,23 +12,30 @@ from datetime import timedelta
|
||||
import logging
|
||||
import hashlib
|
||||
from random import SystemRandom
|
||||
import os
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import ATTR_ENTITY_PICTURE
|
||||
from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_EN_MOTION = 'enable_motion_detection'
|
||||
SERVICE_DISEN_MOTION = 'disable_motion_detection'
|
||||
DOMAIN = 'camera'
|
||||
DEPENDENCIES = ['http']
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
@@ -38,11 +45,32 @@ STATE_RECORDING = 'recording'
|
||||
STATE_STREAMING = 'streaming'
|
||||
STATE_IDLE = 'idle'
|
||||
|
||||
DEFAULT_CONTENT_TYPE = 'image/jpeg'
|
||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
||||
|
||||
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
|
||||
_RND = SystemRandom()
|
||||
|
||||
CAMERA_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
|
||||
|
||||
@bind_hass
|
||||
def enable_motion_detection(hass, entity_id=None):
|
||||
"""Enable Motion Detection."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
DOMAIN, SERVICE_EN_MOTION, data))
|
||||
|
||||
|
||||
@bind_hass
|
||||
def disable_motion_detection(hass, entity_id=None):
|
||||
"""Disable Motion Detection."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
DOMAIN, SERVICE_DISEN_MOTION, data))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_get_image(hass, entity_id, timeout=10):
|
||||
@@ -92,6 +120,44 @@ def async_setup(hass, config):
|
||||
hass.async_add_job(entity.async_update_ha_state())
|
||||
|
||||
async_track_time_interval(hass, update_tokens, TOKEN_CHANGE_INTERVAL)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_camera_service(service):
|
||||
"""Handle calls to the camera services."""
|
||||
target_cameras = component.async_extract_from_service(service)
|
||||
|
||||
for camera in target_cameras:
|
||||
if service.service == SERVICE_EN_MOTION:
|
||||
yield from camera.async_enable_motion_detection()
|
||||
elif service.service == SERVICE_DISEN_MOTION:
|
||||
yield from camera.async_disable_motion_detection()
|
||||
|
||||
update_tasks = []
|
||||
for camera in target_cameras:
|
||||
if not camera.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.async_add_job(
|
||||
camera.async_update_ha_state(True))
|
||||
if hasattr(camera, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
else:
|
||||
yield from update_coro
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_EN_MOTION, async_handle_camera_service,
|
||||
descriptions.get(SERVICE_EN_MOTION), schema=CAMERA_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_DISEN_MOTION, async_handle_camera_service,
|
||||
descriptions.get(SERVICE_DISEN_MOTION), schema=CAMERA_SERVICE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -101,6 +167,7 @@ class Camera(Entity):
|
||||
def __init__(self):
|
||||
"""Initialize a camera."""
|
||||
self.is_streaming = False
|
||||
self.content_type = DEFAULT_CONTENT_TYPE
|
||||
self.access_tokens = collections.deque([], 2)
|
||||
self.async_update_token()
|
||||
|
||||
@@ -124,6 +191,11 @@ class Camera(Entity):
|
||||
"""Return the camera brand."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Return the camera motion detection status."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Return the camera model."""
|
||||
@@ -149,16 +221,17 @@ class Camera(Entity):
|
||||
response = web.StreamResponse()
|
||||
|
||||
response.content_type = ('multipart/x-mixed-replace; '
|
||||
'boundary=--jpegboundary')
|
||||
'boundary=--frameboundary')
|
||||
yield from response.prepare(request)
|
||||
|
||||
def write(img_bytes):
|
||||
"""Write image to stream."""
|
||||
response.write(bytes(
|
||||
'--jpegboundary\r\n'
|
||||
'Content-Type: image/jpeg\r\n'
|
||||
'--frameboundary\r\n'
|
||||
'Content-Type: {}\r\n'
|
||||
'Content-Length: {}\r\n\r\n'.format(
|
||||
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n')
|
||||
self.content_type, len(img_bytes)),
|
||||
'utf-8') + img_bytes + b'\r\n')
|
||||
|
||||
last_image = None
|
||||
|
||||
@@ -196,8 +269,23 @@ class Camera(Entity):
|
||||
return STATE_RECORDING
|
||||
elif self.is_streaming:
|
||||
return STATE_STREAMING
|
||||
else:
|
||||
return STATE_IDLE
|
||||
return STATE_IDLE
|
||||
|
||||
def enable_motion_detection(self):
|
||||
"""Enable motion detection in the camera."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_enable_motion_detection(self):
|
||||
"""Call the job and enable motion detection."""
|
||||
return self.hass.async_add_job(self.enable_motion_detection)
|
||||
|
||||
def disable_motion_detection(self):
|
||||
"""Disable motion detection in camera."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_disable_motion_detection(self):
|
||||
"""Call the job and disable motion detection."""
|
||||
return self.hass.async_add_job(self.disable_motion_detection)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
@@ -212,6 +300,9 @@ class Camera(Entity):
|
||||
if self.brand:
|
||||
attr['brand'] = self.brand
|
||||
|
||||
if self.motion_detection_enabled:
|
||||
attr['motion_detection'] = self.motion_detection_enabled
|
||||
|
||||
return attr
|
||||
|
||||
@callback
|
||||
@@ -269,7 +360,8 @@ class CameraImageView(CameraView):
|
||||
image = yield from camera.async_camera_image()
|
||||
|
||||
if image:
|
||||
return web.Response(body=image, content_type='image/jpeg')
|
||||
return web.Response(body=image,
|
||||
content_type=camera.content_type)
|
||||
|
||||
return web.Response(status=500)
|
||||
|
||||
|
||||
@@ -7,108 +7,59 @@ https://home-assistant.io/components/camera.amcrest/
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.amcrest import (
|
||||
STREAM_SOURCE_LIST, TIMEOUT)
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_aiohttp_proxy_web,
|
||||
async_aiohttp_proxy_stream)
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.2.0']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
DEPENDENCIES = ['amcrest', 'ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_RESOLUTION = 'resolution'
|
||||
CONF_STREAM_SOURCE = 'stream_source'
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
|
||||
DEFAULT_NAME = 'Amcrest Camera'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_RESOLUTION = 'high'
|
||||
DEFAULT_STREAM_SOURCE = 'mjpeg'
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
|
||||
RESOLUTION_LIST = {
|
||||
'high': 0,
|
||||
'low': 1,
|
||||
}
|
||||
|
||||
STREAM_SOURCE_LIST = {
|
||||
'mjpeg': 0,
|
||||
'snapshot': 1,
|
||||
'rtsp': 2,
|
||||
}
|
||||
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
TIMEOUT = 5
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
|
||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up an Amcrest IP Camera."""
|
||||
from amcrest import AmcrestCamera
|
||||
camera = AmcrestCamera(
|
||||
config.get(CONF_HOST), config.get(CONF_PORT),
|
||||
config.get(CONF_USERNAME), config.get(CONF_PASSWORD)).camera
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
try:
|
||||
camera.current_time
|
||||
# pylint: disable=broad-except
|
||||
except Exception as ex:
|
||||
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
||||
persistent_notification.create(
|
||||
hass, 'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
device = discovery_info['device']
|
||||
authentication = discovery_info['authentication']
|
||||
ffmpeg_arguments = discovery_info['ffmpeg_arguments']
|
||||
name = discovery_info['name']
|
||||
resolution = discovery_info['resolution']
|
||||
stream_source = discovery_info['stream_source']
|
||||
|
||||
async_add_devices([
|
||||
AmcrestCam(hass,
|
||||
name,
|
||||
device,
|
||||
authentication,
|
||||
ffmpeg_arguments,
|
||||
stream_source,
|
||||
resolution)], True)
|
||||
|
||||
add_devices([AmcrestCam(hass, config, camera)])
|
||||
return True
|
||||
|
||||
|
||||
class AmcrestCam(Camera):
|
||||
"""An implementation of an Amcrest IP camera."""
|
||||
|
||||
def __init__(self, hass, device_info, camera):
|
||||
def __init__(self, hass, name, camera, authentication,
|
||||
ffmpeg_arguments, stream_source, resolution):
|
||||
"""Initialize an Amcrest camera."""
|
||||
super(AmcrestCam, self).__init__()
|
||||
self._name = name
|
||||
self._camera = camera
|
||||
self._base_url = self._camera.get_base_url()
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
|
||||
self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)]
|
||||
self._stream_source = STREAM_SOURCE_LIST[
|
||||
device_info.get(CONF_STREAM_SOURCE)
|
||||
]
|
||||
self._token = self._auth = aiohttp.BasicAuth(
|
||||
device_info.get(CONF_USERNAME),
|
||||
password=device_info.get(CONF_PASSWORD)
|
||||
)
|
||||
self._ffmpeg_arguments = ffmpeg_arguments
|
||||
self._stream_source = stream_source
|
||||
self._resolution = resolution
|
||||
self._token = self._auth = authentication
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
|
||||
@@ -6,32 +6,32 @@ https://home-assistant.io/components/camera.arlo/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.components.arlo import DEFAULT_BRAND
|
||||
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
from homeassistant.components.arlo import DEFAULT_BRAND, DATA_ARLO
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_aiohttp_proxy_stream)
|
||||
|
||||
DEPENDENCIES = ['arlo', 'ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
ARLO_MODE_ARMED = 'armed'
|
||||
ARLO_MODE_DISARMED = 'disarmed'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS):
|
||||
cv.string,
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up an Arlo IP Camera."""
|
||||
arlo = hass.data.get('arlo')
|
||||
arlo = hass.data.get(DATA_ARLO)
|
||||
if not arlo:
|
||||
return False
|
||||
|
||||
@@ -40,7 +40,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
cameras.append(ArloCam(hass, camera, config))
|
||||
|
||||
async_add_devices(cameras, True)
|
||||
return True
|
||||
|
||||
|
||||
class ArloCam(Camera):
|
||||
@@ -49,14 +48,14 @@ class ArloCam(Camera):
|
||||
def __init__(self, hass, camera, device_info):
|
||||
"""Initialize an Arlo camera."""
|
||||
super().__init__()
|
||||
|
||||
self._camera = camera
|
||||
self._name = self._camera.name
|
||||
self._motion_status = False
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
"""Return a still image response from the camera."""
|
||||
return self._camera.last_image
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -90,3 +89,36 @@ class ArloCam(Camera):
|
||||
def brand(self):
|
||||
"""Camera brand."""
|
||||
return DEFAULT_BRAND
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Camera should poll periodically."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Camera Motion Detection Status."""
|
||||
return self._motion_status
|
||||
|
||||
def set_base_station_mode(self, mode):
|
||||
"""Set the mode in the base station."""
|
||||
# Get the list of base stations identified by library
|
||||
base_stations = self.hass.data[DATA_ARLO].base_stations
|
||||
|
||||
# Some Arlo cameras does not have basestation
|
||||
# So check if there is base station detected first
|
||||
# if yes, then choose the primary base station
|
||||
# Set the mode on the chosen base station
|
||||
if base_stations:
|
||||
primary_base_station = base_stations[0]
|
||||
primary_base_station.mode = mode
|
||||
|
||||
def enable_motion_detection(self):
|
||||
"""Enable the Motion detection in base station (Arm)."""
|
||||
self._motion_status = True
|
||||
self.set_base_station_mode(ARLO_MODE_ARMED)
|
||||
|
||||
def disable_motion_detection(self):
|
||||
"""Disable the motion detection in base station (Disarm)."""
|
||||
self._motion_status = False
|
||||
self.set_base_station_mode(ARLO_MODE_DISARMED)
|
||||
|
||||
@@ -7,15 +7,16 @@ https://home-assistant.io/components/camera.axis/
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.components.camera.mjpeg import (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['axis']
|
||||
DOMAIN = 'axis'
|
||||
DEPENDENCIES = [DOMAIN]
|
||||
|
||||
|
||||
def _get_image_url(host, mode):
|
||||
@@ -27,12 +28,29 @@ def _get_image_url(host, mode):
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Axis camera."""
|
||||
device_info = {
|
||||
CONF_NAME: discovery_info['name'],
|
||||
CONF_USERNAME: discovery_info['username'],
|
||||
CONF_PASSWORD: discovery_info['password'],
|
||||
CONF_MJPEG_URL: _get_image_url(discovery_info['host'], 'mjpeg'),
|
||||
CONF_STILL_IMAGE_URL: _get_image_url(discovery_info['host'], 'single'),
|
||||
config = {
|
||||
CONF_NAME: discovery_info[CONF_NAME],
|
||||
CONF_USERNAME: discovery_info[CONF_USERNAME],
|
||||
CONF_PASSWORD: discovery_info[CONF_PASSWORD],
|
||||
CONF_MJPEG_URL: _get_image_url(discovery_info[CONF_HOST], 'mjpeg'),
|
||||
CONF_STILL_IMAGE_URL: _get_image_url(discovery_info[CONF_HOST],
|
||||
'single'),
|
||||
CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION,
|
||||
}
|
||||
add_devices([MjpegCamera(hass, device_info)])
|
||||
add_devices([AxisCamera(hass, config)])
|
||||
|
||||
|
||||
class AxisCamera(MjpegCamera):
|
||||
"""AxisCamera class."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize Axis Communications camera component."""
|
||||
super().__init__(hass, config)
|
||||
async_dispatcher_connect(hass,
|
||||
DOMAIN + '_' + config[CONF_NAME] + '_new_ip',
|
||||
self._new_ip)
|
||||
|
||||
def _new_ip(self, host):
|
||||
"""Set new IP for video stream."""
|
||||
self._mjpeg_url = _get_image_url(host, 'mjpeg')
|
||||
self._still_image_url = _get_image_url(host, 'mjpeg')
|
||||
|
||||
@@ -5,25 +5,29 @@ For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import os
|
||||
|
||||
import logging
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.camera import Camera
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Demo camera platform."""
|
||||
add_devices([
|
||||
DemoCamera('Demo camera')
|
||||
DemoCamera(hass, config, 'Demo camera')
|
||||
])
|
||||
|
||||
|
||||
class DemoCamera(Camera):
|
||||
"""The representation of a Demo camera."""
|
||||
|
||||
def __init__(self, name):
|
||||
def __init__(self, hass, config, name):
|
||||
"""Initialize demo camera component."""
|
||||
super().__init__()
|
||||
self._parent = hass
|
||||
self._name = name
|
||||
self._motion_status = False
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a faked still image response."""
|
||||
@@ -38,3 +42,21 @@ class DemoCamera(Camera):
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Camera should poll periodically."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Camera Motion Detection Status."""
|
||||
return self._motion_status
|
||||
|
||||
def enable_motion_detection(self):
|
||||
"""Enable the Motion detection in base station (Arm)."""
|
||||
self._motion_status = True
|
||||
|
||||
def disable_motion_detection(self):
|
||||
"""Disable the motion detection in base station (Disarm)."""
|
||||
self._motion_status = False
|
||||
|
||||
@@ -17,13 +17,15 @@ from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
|
||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.components.camera import (
|
||||
PLATFORM_SCHEMA, DEFAULT_CONTENT_TYPE, Camera)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_CONTENT_TYPE = 'content_type'
|
||||
CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change'
|
||||
CONF_STILL_IMAGE_URL = 'still_image_url'
|
||||
|
||||
@@ -37,6 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -59,6 +62,7 @@ class GenericCamera(Camera):
|
||||
self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
|
||||
self._still_image_url.hass = hass
|
||||
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
|
||||
self.content_type = device_info[CONF_CONTENT_TYPE]
|
||||
|
||||
username = device_info.get(CONF_USERNAME)
|
||||
password = device_info.get(CONF_PASSWORD)
|
||||
|
||||
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.local_file/
|
||||
"""
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -46,6 +47,10 @@ class LocalFile(Camera):
|
||||
|
||||
self._name = name
|
||||
self._file_path = file_path
|
||||
# Set content type of local file
|
||||
content, _ = mimetypes.guess_type(file_path)
|
||||
if content is not None:
|
||||
self.content_type = content
|
||||
|
||||
def camera_image(self):
|
||||
"""Return image response."""
|
||||
|
||||
@@ -113,8 +113,7 @@ class NetatmoCamera(Camera):
|
||||
return "Presence"
|
||||
elif self._cameratype == "NACamera":
|
||||
return "Welcome"
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
|
||||
@@ -6,6 +6,7 @@ https://home-assistant.io/components/camera.onvif/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -54,6 +55,7 @@ class ONVIFCamera(Camera):
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a ONVIF camera."""
|
||||
from onvif import ONVIFService
|
||||
import onvif
|
||||
super().__init__()
|
||||
|
||||
self._name = config.get(CONF_NAME)
|
||||
@@ -63,7 +65,7 @@ class ONVIFCamera(Camera):
|
||||
config.get(CONF_HOST), config.get(CONF_PORT)),
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
'{}/deps/onvif/wsdl/media.wsdl'.format(hass.config.config_dir)
|
||||
'{}/wsdl/media.wsdl'.format(os.path.dirname(onvif.__file__))
|
||||
)
|
||||
self._input = media.GetStreamUri().Uri
|
||||
_LOGGER.debug("ONVIF Camera Using the following URL for %s: %s",
|
||||
|
||||
17
homeassistant/components/camera/services.yaml
Normal file
17
homeassistant/components/camera/services.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
# Describes the format for available camera services
|
||||
|
||||
enable_motion_detection:
|
||||
description: Enable the motion detection in a camera
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to enable motion detection
|
||||
example: 'camera.living_room_camera'
|
||||
|
||||
disable_motion_detection:
|
||||
description: Disable the motion detection in a camera
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to disable motion detection
|
||||
example: 'camera.living_room_camera'
|
||||
@@ -54,7 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.error("Unable to connect to NVR: %s", str(ex))
|
||||
return False
|
||||
|
||||
identifier = nvrconn.server_version >= (3, 2, 0) and 'id' or 'uuid'
|
||||
identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid'
|
||||
# Filter out airCam models, which are not supported in the latest
|
||||
# version of UnifiVideo and which are EOL by Ubiquiti
|
||||
cameras = [
|
||||
|
||||
@@ -24,22 +24,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if not os.access(directory_path, os.R_OK):
|
||||
_LOGGER.error("file path %s is not readable", directory_path)
|
||||
return False
|
||||
hub.update_smartcam()
|
||||
hub.update_overview()
|
||||
smartcams = []
|
||||
smartcams.extend([
|
||||
VerisureSmartcam(hass, value.deviceLabel, directory_path)
|
||||
for value in hub.smartcam_status.values()])
|
||||
VerisureSmartcam(hass, device_label, directory_path)
|
||||
for device_label in hub.get(
|
||||
"$.customerImageCameras[*].deviceLabel")])
|
||||
add_devices(smartcams)
|
||||
|
||||
|
||||
class VerisureSmartcam(Camera):
|
||||
"""Representation of a Verisure camera."""
|
||||
|
||||
def __init__(self, hass, device_id, directory_path):
|
||||
def __init__(self, hass, device_label, directory_path):
|
||||
"""Initialize Verisure File Camera component."""
|
||||
super().__init__()
|
||||
|
||||
self._device_id = device_id
|
||||
self._device_label = device_label
|
||||
self._directory_path = directory_path
|
||||
self._image = None
|
||||
self._image_id = None
|
||||
@@ -58,28 +59,27 @@ class VerisureSmartcam(Camera):
|
||||
|
||||
def check_imagelist(self):
|
||||
"""Check the contents of the image list."""
|
||||
hub.update_smartcam_imagelist()
|
||||
if (self._device_id not in hub.smartcam_dict or
|
||||
not hub.smartcam_dict[self._device_id]):
|
||||
hub.update_smartcam_imageseries()
|
||||
image_ids = hub.get_image_info(
|
||||
"$.imageSeries[?(@.deviceLabel=='%s')].image[0].imageId",
|
||||
self._device_label)
|
||||
if not image_ids:
|
||||
return
|
||||
images = hub.smartcam_dict[self._device_id]
|
||||
new_image_id = images[0]
|
||||
_LOGGER.debug("self._device_id=%s, self._images=%s, "
|
||||
"self._new_image_id=%s", self._device_id,
|
||||
images, new_image_id)
|
||||
new_image_id = image_ids[0]
|
||||
if (new_image_id == '-1' or
|
||||
self._image_id == new_image_id):
|
||||
_LOGGER.debug("The image is the same, or loading image_id")
|
||||
return
|
||||
_LOGGER.debug("Download new image %s", new_image_id)
|
||||
hub.my_pages.smartcam.download_image(
|
||||
self._device_id, new_image_id, self._directory_path)
|
||||
new_image_path = os.path.join(
|
||||
self._directory_path, '{}{}'.format(new_image_id, '.jpg'))
|
||||
hub.session.download_image(
|
||||
self._device_label, new_image_id, new_image_path)
|
||||
_LOGGER.debug("Old image_id=%s", self._image_id)
|
||||
self.delete_image(self)
|
||||
|
||||
self._image_id = new_image_id
|
||||
self._image = os.path.join(
|
||||
self._directory_path, '{}{}'.format(self._image_id, '.jpg'))
|
||||
self._image = new_image_path
|
||||
|
||||
def delete_image(self, event):
|
||||
"""Delete an old image."""
|
||||
@@ -95,4 +95,6 @@ class VerisureSmartcam(Camera):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return hub.smartcam_status[self._device_id].location
|
||||
return hub.get_first(
|
||||
"$.customerImageCameras[?(@.deviceLabel=='%s')].area",
|
||||
self._device_label)
|
||||
|
||||
@@ -14,6 +14,7 @@ from numbers import Number
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -114,6 +115,7 @@ SET_SWING_MODE_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_away_mode(hass, away_mode, entity_id=None):
|
||||
"""Turn all or specified climate devices away mode on."""
|
||||
data = {
|
||||
@@ -126,6 +128,7 @@ def set_away_mode(hass, away_mode, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_hold_mode(hass, hold_mode, entity_id=None):
|
||||
"""Set new hold mode."""
|
||||
data = {
|
||||
@@ -138,6 +141,7 @@ def set_hold_mode(hass, hold_mode, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_HOLD_MODE, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_aux_heat(hass, aux_heat, entity_id=None):
|
||||
"""Turn all or specified climate devices auxillary heater on."""
|
||||
data = {
|
||||
@@ -150,6 +154,7 @@ def set_aux_heat(hass, aux_heat, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_temperature(hass, temperature=None, entity_id=None,
|
||||
target_temp_high=None, target_temp_low=None,
|
||||
operation_mode=None):
|
||||
@@ -167,6 +172,7 @@ def set_temperature(hass, temperature=None, entity_id=None,
|
||||
hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, kwargs)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_humidity(hass, humidity, entity_id=None):
|
||||
"""Set new target humidity."""
|
||||
data = {ATTR_HUMIDITY: humidity}
|
||||
@@ -177,6 +183,7 @@ def set_humidity(hass, humidity, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_fan_mode(hass, fan, entity_id=None):
|
||||
"""Set all or specified climate devices fan mode on."""
|
||||
data = {ATTR_FAN_MODE: fan}
|
||||
@@ -187,6 +194,7 @@ def set_fan_mode(hass, fan, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_operation_mode(hass, operation_mode, entity_id=None):
|
||||
"""Set new target operation mode."""
|
||||
data = {ATTR_OPERATION_MODE: operation_mode}
|
||||
@@ -197,6 +205,7 @@ def set_operation_mode(hass, operation_mode, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_swing_mode(hass, swing_mode, entity_id=None):
|
||||
"""Set new target swing mode."""
|
||||
data = {ATTR_SWING_MODE: swing_mode}
|
||||
@@ -398,16 +407,14 @@ class ClimateDevice(Entity):
|
||||
"""Return the current state."""
|
||||
if self.current_operation:
|
||||
return self.current_operation
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"""Return the precision of the system."""
|
||||
if self.unit_of_measurement == TEMP_CELSIUS:
|
||||
return PRECISION_TENTHS
|
||||
else:
|
||||
return PRECISION_WHOLE
|
||||
return PRECISION_WHOLE
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
@@ -693,8 +700,14 @@ class ClimateDevice(Entity):
|
||||
|
||||
def _convert_for_display(self, temp):
|
||||
"""Convert temperature into preferred units for display purposes."""
|
||||
if temp is None or not isinstance(temp, Number):
|
||||
if temp is None:
|
||||
return temp
|
||||
|
||||
# if the temperature is not a number this can cause issues
|
||||
# with polymer components, so bail early there.
|
||||
if not isinstance(temp, Number):
|
||||
raise TypeError("Temperature is not a number: %s" % temp)
|
||||
|
||||
if self.temperature_unit != self.unit_of_measurement:
|
||||
temp = convert_temperature(
|
||||
temp, self.temperature_unit, self.unit_of_measurement)
|
||||
@@ -703,6 +716,5 @@ class ClimateDevice(Entity):
|
||||
return round(temp * 2) / 2.0
|
||||
elif self.precision == PRECISION_TENTHS:
|
||||
return round(temp, 1)
|
||||
else:
|
||||
# PRECISION_WHOLE as a fall back
|
||||
return round(temp)
|
||||
# PRECISION_WHOLE as a fall back
|
||||
return round(temp)
|
||||
|
||||
@@ -151,16 +151,14 @@ class Thermostat(ClimateDevice):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return int(self.thermostat['runtime']['desiredHeat'] / 10)
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the upper bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return int(self.thermostat['runtime']['desiredCool'] / 10)
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
@@ -171,8 +169,7 @@ class Thermostat(ClimateDevice):
|
||||
return int(self.thermostat['runtime']['desiredHeat'] / 10)
|
||||
elif self.current_operation == STATE_COOL:
|
||||
return int(self.thermostat['runtime']['desiredCool'] / 10)
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def desired_fan_mode(self):
|
||||
@@ -184,8 +181,7 @@ class Thermostat(ClimateDevice):
|
||||
"""Return the current fan state."""
|
||||
if 'fan' in self.thermostat['equipmentStatus']:
|
||||
return STATE_ON
|
||||
else:
|
||||
return STATE_OFF
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
@@ -199,15 +195,13 @@ class Thermostat(ClimateDevice):
|
||||
int(event['startDate'][0:4]) <= 1:
|
||||
# A temporary hold from away climate is a hold
|
||||
return 'away'
|
||||
else:
|
||||
# A permanent hold from away climate is away_mode
|
||||
return None
|
||||
# A permanent hold from away climate is away_mode
|
||||
return None
|
||||
elif event['holdClimateRef'] != "":
|
||||
# Any other hold based on climate
|
||||
return event['holdClimateRef']
|
||||
else:
|
||||
# Any hold not based on a climate is a temp hold
|
||||
return TEMPERATURE_HOLD
|
||||
# Any hold not based on a climate is a temp hold
|
||||
return TEMPERATURE_HOLD
|
||||
elif event['type'].startswith('auto'):
|
||||
# All auto modes are treated as holds
|
||||
return event['type'][4:].lower()
|
||||
@@ -222,8 +216,7 @@ class Thermostat(ClimateDevice):
|
||||
if self.operation_mode == 'auxHeatOnly' or \
|
||||
self.operation_mode == 'heatPump':
|
||||
return STATE_HEAT
|
||||
else:
|
||||
return self.operation_mode
|
||||
return self.operation_mode
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
@@ -384,8 +377,7 @@ class Thermostat(ClimateDevice):
|
||||
# add further conditions if other hold durations should be
|
||||
# supported; note that this should not include 'indefinite'
|
||||
# as an indefinite away hold is interpreted as away_mode
|
||||
else:
|
||||
return 'nextTransition'
|
||||
return 'nextTransition'
|
||||
|
||||
@property
|
||||
def climate_list(self):
|
||||
|
||||
@@ -145,4 +145,4 @@ class Flexit(ClimateDevice):
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new fan mode."""
|
||||
self.unit.set_fan_speed(fan)
|
||||
self.unit.set_fan_speed(self._fan_list.index(fan))
|
||||
|
||||
@@ -12,7 +12,8 @@ import voluptuous as vol
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components import switch
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA)
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
|
||||
STATE_AUTO)
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
|
||||
CONF_NAME)
|
||||
@@ -87,6 +88,7 @@ class GenericThermostat(ClimateDevice):
|
||||
self.min_cycle_duration = min_cycle_duration
|
||||
self._tolerance = tolerance
|
||||
self._keep_alive = keep_alive
|
||||
self._enabled = True
|
||||
|
||||
self._active = False
|
||||
self._cur_temp = None
|
||||
@@ -131,18 +133,39 @@ class GenericThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if not self._enabled:
|
||||
return STATE_OFF
|
||||
if self.ac_mode:
|
||||
cooling = self._active and self._is_device_active
|
||||
return STATE_COOL if cooling else STATE_IDLE
|
||||
else:
|
||||
heating = self._active and self._is_device_active
|
||||
return STATE_HEAT if heating else STATE_IDLE
|
||||
|
||||
heating = self._active and self._is_device_active
|
||||
return STATE_HEAT if heating else STATE_IDLE
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temp
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return [STATE_AUTO, STATE_OFF]
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_AUTO:
|
||||
self._enabled = True
|
||||
elif operation_mode == STATE_OFF:
|
||||
self._enabled = False
|
||||
if self._is_device_active:
|
||||
switch.async_turn_off(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
_LOGGER.error('Unrecognized operation mode: %s', operation_mode)
|
||||
return
|
||||
# Ensure we updae the current operation after changing the mode
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
@@ -159,9 +182,9 @@ class GenericThermostat(ClimateDevice):
|
||||
# pylint: disable=no-member
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
else:
|
||||
# get default temp from super class
|
||||
return ClimateDevice.min_temp.fget(self)
|
||||
|
||||
# get default temp from super class
|
||||
return ClimateDevice.min_temp.fget(self)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
@@ -169,9 +192,9 @@ class GenericThermostat(ClimateDevice):
|
||||
# pylint: disable=no-member
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
else:
|
||||
# Get default temp from super class
|
||||
return ClimateDevice.max_temp.fget(self)
|
||||
|
||||
# Get default temp from super class
|
||||
return ClimateDevice.max_temp.fget(self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_sensor_changed(self, entity_id, old_state, new_state):
|
||||
@@ -221,6 +244,9 @@ class GenericThermostat(ClimateDevice):
|
||||
if not self._active:
|
||||
return
|
||||
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
if self.min_cycle_duration:
|
||||
if self._is_device_active:
|
||||
current_state = STATE_ON
|
||||
|
||||
@@ -46,8 +46,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMThermostat(hass, config)
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMThermostat(hass, conf)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
if region == 'us':
|
||||
return _setup_us(username, password, config, add_devices)
|
||||
else:
|
||||
return _setup_round(username, password, config, add_devices)
|
||||
|
||||
return _setup_round(username, password, config, add_devices)
|
||||
|
||||
|
||||
def _setup_round(username, password, config, add_devices):
|
||||
@@ -251,8 +251,7 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._device.system_mode == 'cool':
|
||||
return self._device.setpoint_cool
|
||||
else:
|
||||
return self._device.setpoint_heat
|
||||
return self._device.setpoint_heat
|
||||
|
||||
@property
|
||||
def current_operation(self: ClimateDevice) -> str:
|
||||
|
||||
@@ -10,7 +10,6 @@ import logging
|
||||
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
|
||||
from homeassistant.components.maxcube import MAXCUBE_HANDLE
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -140,7 +139,7 @@ class MaxCubeClimate(ClimateDevice):
|
||||
def map_temperature_max_hass(temperature):
|
||||
"""Map Temperature from MAX! to HASS."""
|
||||
if temperature is None:
|
||||
return STATE_UNKNOWN
|
||||
return 0.0
|
||||
|
||||
return temperature
|
||||
|
||||
|
||||
@@ -67,7 +67,12 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._values.get(self.gateway.const.SetReq.V_TEMP)
|
||||
value = self._values.get(self.gateway.const.SetReq.V_TEMP)
|
||||
|
||||
if value is not None:
|
||||
value = float(value)
|
||||
|
||||
return value
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
@@ -79,21 +84,21 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
||||
temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL)
|
||||
if temp is None:
|
||||
temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
|
||||
return temp
|
||||
return float(temp)
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the highbound target temperature we try to reach."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
if set_req.V_HVAC_SETPOINT_HEAT in self._values:
|
||||
return self._values.get(set_req.V_HVAC_SETPOINT_COOL)
|
||||
return float(self._values.get(set_req.V_HVAC_SETPOINT_COOL))
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lowbound target temperature we try to reach."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
if set_req.V_HVAC_SETPOINT_COOL in self._values:
|
||||
return self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
|
||||
return float(self._values.get(set_req.V_HVAC_SETPOINT_HEAT))
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
|
||||
@@ -109,16 +109,14 @@ class NestThermostat(ClimateDevice):
|
||||
return self._mode
|
||||
elif self._mode == STATE_HEAT_COOL:
|
||||
return STATE_AUTO
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._mode != STATE_HEAT_COOL and not self.is_away_mode_on:
|
||||
return self._target_temperature
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
@@ -129,8 +127,7 @@ class NestThermostat(ClimateDevice):
|
||||
return self._eco_temperature[0]
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
return self._target_temperature[0]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
@@ -141,8 +138,7 @@ class NestThermostat(ClimateDevice):
|
||||
return self._eco_temperature[1]
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
return self._target_temperature[1]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
@@ -188,9 +184,8 @@ class NestThermostat(ClimateDevice):
|
||||
if self._has_fan:
|
||||
# Return whether the fan is on
|
||||
return STATE_ON if self._fan else STATE_AUTO
|
||||
else:
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
|
||||
@@ -119,14 +119,14 @@ class NetatmoThermostat(ClimateDevice):
|
||||
self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None)
|
||||
self._away = False
|
||||
|
||||
def set_temperature(self, endTimeOffset=DEFAULT_TIME_OFFSET, **kwargs):
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature for 2 hours."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
mode = "manual"
|
||||
self._data.thermostatdata.setthermpoint(
|
||||
mode, temperature, endTimeOffset)
|
||||
mode, temperature, DEFAULT_TIME_OFFSET)
|
||||
self._target_temperature = temperature
|
||||
self._away = False
|
||||
|
||||
|
||||
@@ -92,8 +92,7 @@ class ThermostatDevice(ClimateDevice):
|
||||
"""Return current operation i.e. heat, cool, idle."""
|
||||
if self._state:
|
||||
return STATE_HEAT
|
||||
else:
|
||||
return STATE_IDLE
|
||||
return STATE_IDLE
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.components.climate import (
|
||||
from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['radiotherm==1.2']
|
||||
REQUIREMENTS = ['radiotherm==1.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -84,6 +84,7 @@ class RadioThermostat(ClimateDevice):
|
||||
self._name = None
|
||||
self._fmode = None
|
||||
self._tmode = None
|
||||
self._tstate = None
|
||||
self._hold_temp = hold_temp
|
||||
self._away = False
|
||||
self._away_temps = away_temps
|
||||
@@ -135,18 +136,54 @@ class RadioThermostat(ClimateDevice):
|
||||
return self._away
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the thermostat."""
|
||||
self._current_temperature = self.device.temp['raw']
|
||||
"""Update and validate the data from the thermostat."""
|
||||
current_temp = self.device.temp['raw']
|
||||
if current_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid temperature reading")
|
||||
return
|
||||
self._current_temperature = current_temp
|
||||
self._name = self.device.name['raw']
|
||||
self._fmode = self.device.fmode['human']
|
||||
self._tmode = self.device.tmode['human']
|
||||
try:
|
||||
self._fmode = self.device.fmode['human']
|
||||
except AttributeError:
|
||||
_LOGGER.error("Couldn't get valid fan mode reading")
|
||||
try:
|
||||
self._tmode = self.device.tmode['human']
|
||||
except AttributeError:
|
||||
_LOGGER.error("Couldn't get valid thermostat mode reading")
|
||||
try:
|
||||
self._tstate = self.device.tstate['human']
|
||||
except AttributeError:
|
||||
_LOGGER.error("Couldn't get valid thermostat state reading")
|
||||
|
||||
if self._tmode == 'Cool':
|
||||
self._target_temperature = self.device.t_cool['raw']
|
||||
target_temp = self.device.t_cool['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
self._current_operation = STATE_COOL
|
||||
elif self._tmode == 'Heat':
|
||||
self._target_temperature = self.device.t_heat['raw']
|
||||
target_temp = self.device.t_heat['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
self._current_operation = STATE_HEAT
|
||||
elif self._tmode == 'Auto':
|
||||
if self._tstate == 'Cool':
|
||||
target_temp = self.device.t_cool['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
elif self._tstate == 'Heat':
|
||||
target_temp = self.device.t_heat['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
self._current_operation = STATE_AUTO
|
||||
else:
|
||||
self._current_operation = STATE_IDLE
|
||||
|
||||
@@ -159,6 +196,12 @@ class RadioThermostat(ClimateDevice):
|
||||
self.device.t_cool = round(temperature * 2.0) / 2.0
|
||||
elif self._current_operation == STATE_HEAT:
|
||||
self.device.t_heat = round(temperature * 2.0) / 2.0
|
||||
elif self._current_operation == STATE_AUTO:
|
||||
if self._tstate == 'Cool':
|
||||
self.device.t_cool = round(temperature * 2.0) / 2.0
|
||||
elif self._tstate == 'Heat':
|
||||
self.device.t_heat = round(temperature * 2.0) / 2.0
|
||||
|
||||
if self._hold_temp or self._away:
|
||||
self.device.hold = 1
|
||||
else:
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
@@ -52,9 +53,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
yield from client.async_get_devices(_INITIAL_FETCH_FIELDS)):
|
||||
if config[CONF_ID] == ALL or dev['id'] in config[CONF_ID]:
|
||||
devices.append(SensiboClimate(client, dev))
|
||||
except aiohttp.client_exceptions.ClientConnectorError:
|
||||
except (aiohttp.client_exceptions.ClientConnectorError,
|
||||
asyncio.TimeoutError):
|
||||
_LOGGER.exception('Failed to connct to Sensibo servers.')
|
||||
return False
|
||||
raise PlatformNotReady
|
||||
|
||||
if devices:
|
||||
async_add_devices(devices)
|
||||
@@ -115,9 +117,8 @@ class SensiboClimate(ClimateDevice):
|
||||
# We are working in same units as the a/c unit. Use whole degrees
|
||||
# like the API supports.
|
||||
return 1
|
||||
else:
|
||||
# Unit conversion is going on. No point to stick to specific steps.
|
||||
return None
|
||||
# Unit conversion is going on. No point to stick to specific steps.
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
|
||||
@@ -24,6 +24,17 @@ CONST_OVERLAY_MANUAL = 'MANUAL'
|
||||
# the temperature will be reset after a timespan
|
||||
CONST_OVERLAY_TIMER = 'TIMER'
|
||||
|
||||
CONST_MODE_FAN_HIGH = 'HIGH'
|
||||
CONST_MODE_FAN_MIDDLE = 'MIDDLE'
|
||||
CONST_MODE_FAN_LOW = 'LOW'
|
||||
|
||||
FAN_MODES_LIST = {
|
||||
CONST_MODE_FAN_HIGH: 'High',
|
||||
CONST_MODE_FAN_MIDDLE: 'Middle',
|
||||
CONST_MODE_FAN_LOW: 'Low',
|
||||
CONST_MODE_OFF: 'Off',
|
||||
}
|
||||
|
||||
OPERATION_LIST = {
|
||||
CONST_OVERLAY_MANUAL: 'Manual',
|
||||
CONST_OVERLAY_TIMER: 'Timer',
|
||||
@@ -41,7 +52,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
zones = tado.get_zones()
|
||||
except RuntimeError:
|
||||
_LOGGER.error("Unable to get zone info from mytado")
|
||||
return False
|
||||
return
|
||||
|
||||
climate_devices = []
|
||||
for zone in zones:
|
||||
@@ -50,9 +61,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
if climate_devices:
|
||||
add_devices(climate_devices, True)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def create_climate_device(tado, hass, zone, name, zone_id):
|
||||
@@ -60,9 +68,15 @@ def create_climate_device(tado, hass, zone, name, zone_id):
|
||||
capabilities = tado.get_capabilities(zone_id)
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
min_temp = float(capabilities['temperatures']['celsius']['min'])
|
||||
max_temp = float(capabilities['temperatures']['celsius']['max'])
|
||||
ac_mode = capabilities['type'] != 'HEATING'
|
||||
ac_mode = capabilities['type'] == 'AIR_CONDITIONING'
|
||||
|
||||
if ac_mode:
|
||||
temperatures = capabilities['HEAT']['temperatures']
|
||||
else:
|
||||
temperatures = capabilities['temperatures']
|
||||
|
||||
min_temp = float(temperatures['celsius']['min'])
|
||||
max_temp = float(temperatures['celsius']['max'])
|
||||
|
||||
data_id = 'zone {} {}'.format(name, zone_id)
|
||||
device = TadoClimate(tado,
|
||||
@@ -107,7 +121,9 @@ class TadoClimate(ClimateDevice):
|
||||
self._max_temp = max_temp
|
||||
self._target_temp = None
|
||||
self._tolerance = tolerance
|
||||
self._cooling = False
|
||||
|
||||
self._current_fan = CONST_MODE_OFF
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
@@ -129,6 +145,8 @@ class TadoClimate(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current readable operation mode."""
|
||||
if self._cooling:
|
||||
return "Cooling"
|
||||
return OPERATION_LIST.get(self._current_operation)
|
||||
|
||||
@property
|
||||
@@ -136,6 +154,20 @@ class TadoClimate(ClimateDevice):
|
||||
"""Return the list of available operation modes (readable)."""
|
||||
return list(OPERATION_LIST.values())
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
if self.ac_mode:
|
||||
return FAN_MODES_LIST.get(self._current_fan)
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
if self.ac_mode:
|
||||
return list(FAN_MODES_LIST.values())
|
||||
return None
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement used by the platform."""
|
||||
@@ -180,18 +212,16 @@ class TadoClimate(ClimateDevice):
|
||||
"""Return the minimum temperature."""
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
else:
|
||||
# get default temp from super class
|
||||
return super().min_temp
|
||||
# get default temp from super class
|
||||
return super().min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
else:
|
||||
# Get default temp from super class
|
||||
return super().max_temp
|
||||
# Get default temp from super class
|
||||
return super().max_temp
|
||||
|
||||
def update(self):
|
||||
"""Update the state of this climate device."""
|
||||
@@ -205,27 +235,27 @@ class TadoClimate(ClimateDevice):
|
||||
|
||||
if 'sensorDataPoints' in data:
|
||||
sensor_data = data['sensorDataPoints']
|
||||
temperature = float(
|
||||
sensor_data['insideTemperature']['celsius'])
|
||||
humidity = float(
|
||||
sensor_data['humidity']['percentage'])
|
||||
setting = 0
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
|
||||
if 'insideTemperature' in sensor_data:
|
||||
temperature = float(
|
||||
sensor_data['insideTemperature']['celsius'])
|
||||
self._cur_temp = self.hass.config.units.temperature(
|
||||
temperature, unit)
|
||||
|
||||
if 'humidity' in sensor_data:
|
||||
humidity = float(
|
||||
sensor_data['humidity']['percentage'])
|
||||
self._cur_humidity = humidity
|
||||
|
||||
# temperature setting will not exist when device is off
|
||||
if 'temperature' in data['setting'] and \
|
||||
data['setting']['temperature'] is not None:
|
||||
setting = float(
|
||||
data['setting']['temperature']['celsius'])
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
|
||||
self._cur_temp = self.hass.config.units.temperature(
|
||||
temperature, unit)
|
||||
|
||||
self._target_temp = self.hass.config.units.temperature(
|
||||
setting, unit)
|
||||
|
||||
self._cur_humidity = humidity
|
||||
self._target_temp = self.hass.config.units.temperature(
|
||||
setting, unit)
|
||||
|
||||
if 'tadoMode' in data:
|
||||
mode = data['tadoMode']
|
||||
@@ -235,29 +265,46 @@ class TadoClimate(ClimateDevice):
|
||||
power = data['setting']['power']
|
||||
if power == 'OFF':
|
||||
self._current_operation = CONST_MODE_OFF
|
||||
self._current_fan = CONST_MODE_OFF
|
||||
# There is no overlay, the mode will always be
|
||||
# "SMART_SCHEDULE"
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
self._device_is_active = False
|
||||
else:
|
||||
self._device_is_active = True
|
||||
|
||||
if 'overlay' in data and data['overlay'] is not None:
|
||||
overlay = True
|
||||
termination = data['overlay']['termination']['type']
|
||||
else:
|
||||
overlay = False
|
||||
termination = ""
|
||||
overlay = False
|
||||
overlay_data = None
|
||||
termination = self._current_operation
|
||||
cooling = False
|
||||
fan_speed = CONST_MODE_OFF
|
||||
|
||||
# If you set mode manualy to off, there will be an overlay
|
||||
# and a termination, but we want to see the mode "OFF"
|
||||
if 'overlay' in data:
|
||||
overlay_data = data['overlay']
|
||||
overlay = overlay_data is not None
|
||||
|
||||
if overlay and self._device_is_active:
|
||||
# There is an overlay the device is on
|
||||
if overlay:
|
||||
termination = overlay_data['termination']['type']
|
||||
|
||||
if 'setting' in overlay_data:
|
||||
setting_data = overlay_data['setting']
|
||||
setting = setting is not None
|
||||
|
||||
if setting:
|
||||
if 'mode' in setting_data:
|
||||
cooling = setting_data['mode'] == 'COOL'
|
||||
|
||||
if 'fanSpeed' in setting_data:
|
||||
fan_speed = setting_data['fanSpeed']
|
||||
|
||||
if self._device_is_active:
|
||||
# If you set mode manualy to off, there will be an overlay
|
||||
# and a termination, but we want to see the mode "OFF"
|
||||
self._overlay_mode = termination
|
||||
self._current_operation = termination
|
||||
else:
|
||||
# There is no overlay, the mode will always be
|
||||
# "SMART_SCHEDULE"
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
self._cooling = cooling
|
||||
self._current_fan = fan_speed
|
||||
|
||||
def _control_heating(self):
|
||||
"""Send new target temperature to mytado."""
|
||||
|
||||
@@ -111,8 +111,8 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
# This will address both possibilities
|
||||
if self.wink.current_humidity() < 1:
|
||||
return self.wink.current_humidity() * 100
|
||||
else:
|
||||
return self.wink.current_humidity()
|
||||
return self.wink.current_humidity()
|
||||
return None
|
||||
|
||||
@property
|
||||
def external_temperature(self):
|
||||
@@ -175,10 +175,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
return self.wink.current_max_set_point()
|
||||
elif self.current_operation == STATE_HEAT:
|
||||
return self.wink.current_min_set_point()
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
@@ -206,8 +203,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
return True
|
||||
elif self.wink.current_hvac_mode() == 'aux' and not self.wink.is_on():
|
||||
return False
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
@@ -270,9 +266,8 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
return STATE_ON
|
||||
elif self.wink.current_fan_mode() == 'auto':
|
||||
return STATE_AUTO
|
||||
else:
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
@@ -451,8 +446,7 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
return SPEED_MEDIUM
|
||||
elif speed <= 1.0 and speed > 0.8:
|
||||
return SPEED_HIGH
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
|
||||
@@ -154,8 +154,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
return TEMP_CELSIUS
|
||||
elif self._unit == 'F':
|
||||
return TEMP_FAHRENHEIT
|
||||
else:
|
||||
return self._unit
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
|
||||
134
homeassistant/components/comfoconnect.py
Normal file
134
homeassistant/components/comfoconnect.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Support to control a Zehnder ComfoAir Q350/450/600 ventilation unit.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/comfoconnect/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_TOKEN, CONF_PIN, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers import (discovery)
|
||||
from homeassistant.helpers.dispatcher import (dispatcher_send)
|
||||
|
||||
REQUIREMENTS = ['pycomfoconnect==0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'comfoconnect'
|
||||
|
||||
SIGNAL_COMFOCONNECT_UPDATE_RECEIVED = 'comfoconnect_update_received'
|
||||
|
||||
ATTR_CURRENT_TEMPERATURE = 'current_temperature'
|
||||
ATTR_CURRENT_HUMIDITY = 'current_humidity'
|
||||
ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature'
|
||||
ATTR_OUTSIDE_HUMIDITY = 'outside_humidity'
|
||||
ATTR_AIR_FLOW_SUPPLY = 'air_flow_supply'
|
||||
ATTR_AIR_FLOW_EXHAUST = 'air_flow_exhaust'
|
||||
|
||||
CONF_USER_AGENT = 'user_agent'
|
||||
|
||||
DEFAULT_NAME = 'ComfoAirQ'
|
||||
DEFAULT_PIN = 0
|
||||
DEFAULT_TOKEN = '00000000000000000000000000000001'
|
||||
DEFAULT_USER_AGENT = 'Home Assistant'
|
||||
|
||||
DEVICE = None
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_TOKEN, default=DEFAULT_TOKEN):
|
||||
vol.Length(min=32, max=32, msg='invalid token'),
|
||||
vol.Optional(CONF_USER_AGENT, default=DEFAULT_USER_AGENT): cv.string,
|
||||
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the ComfoConnect bridge."""
|
||||
from pycomfoconnect import (Bridge)
|
||||
|
||||
conf = config[DOMAIN]
|
||||
host = conf.get(CONF_HOST)
|
||||
name = conf.get(CONF_NAME)
|
||||
token = conf.get(CONF_TOKEN)
|
||||
user_agent = conf.get(CONF_USER_AGENT)
|
||||
pin = conf.get(CONF_PIN)
|
||||
|
||||
# Run discovery on the configured ip
|
||||
bridges = Bridge.discover(host)
|
||||
if not bridges:
|
||||
_LOGGER.error("Could not connect to ComfoConnect bridge on %s", host)
|
||||
return False
|
||||
bridge = bridges[0]
|
||||
_LOGGER.info("Bridge found: %s (%s)", bridge.uuid.hex(), bridge.host)
|
||||
|
||||
# Setup ComfoConnect Bridge
|
||||
ccb = ComfoConnectBridge(hass, bridge, name, token, user_agent, pin)
|
||||
hass.data[DOMAIN] = ccb
|
||||
|
||||
# Start connection with bridge
|
||||
ccb.connect()
|
||||
|
||||
# Schedule disconnect on shutdown
|
||||
def _shutdown(_event):
|
||||
ccb.disconnect()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
|
||||
|
||||
# Load platforms
|
||||
discovery.load_platform(hass, 'fan', DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ComfoConnectBridge(object):
|
||||
"""Representation of a ComfoConnect bridge."""
|
||||
|
||||
def __init__(self, hass, bridge, name, token, friendly_name, pin):
|
||||
"""Initialize the ComfoConnect bridge."""
|
||||
from pycomfoconnect import (ComfoConnect)
|
||||
|
||||
self.data = {}
|
||||
self.name = name
|
||||
self.hass = hass
|
||||
|
||||
self.comfoconnect = ComfoConnect(
|
||||
bridge=bridge, local_uuid=bytes.fromhex(token),
|
||||
local_devicename=friendly_name, pin=pin)
|
||||
self.comfoconnect.callback_sensor = self.sensor_callback
|
||||
|
||||
def connect(self):
|
||||
"""Connect with the bridge."""
|
||||
_LOGGER.debug("Connecting with bridge")
|
||||
self.comfoconnect.connect(True)
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from the bridge."""
|
||||
_LOGGER.debug("Disconnecting from bridge")
|
||||
self.comfoconnect.disconnect()
|
||||
|
||||
def sensor_callback(self, var, value):
|
||||
"""Callback function for sensor updates."""
|
||||
_LOGGER.debug("Got value from bridge: %d = %d", var, value)
|
||||
|
||||
from pycomfoconnect import (
|
||||
SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR)
|
||||
|
||||
if var in [SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR]:
|
||||
self.data[var] = value / 10
|
||||
else:
|
||||
self.data[var] = value
|
||||
|
||||
# Notify listeners that we have received an update
|
||||
dispatcher_send(self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, var)
|
||||
|
||||
def subscribe_sensor(self, sensor_id):
|
||||
"""Subscribe for the specified sensor."""
|
||||
self.comfoconnect.register_sensor(sensor_id)
|
||||
@@ -12,6 +12,7 @@ import logging
|
||||
from homeassistant.core import callback as async_callback
|
||||
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
|
||||
ATTR_ENTITY_PICTURE
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
|
||||
@@ -37,6 +38,7 @@ STATE_CONFIGURE = 'configure'
|
||||
STATE_CONFIGURED = 'configured'
|
||||
|
||||
|
||||
@bind_hass
|
||||
def request_config(
|
||||
hass, name, callback, description=None, description_image=None,
|
||||
submit_caption=None, fields=None, link_name=None, link_url=None,
|
||||
|
||||
@@ -4,6 +4,7 @@ Support for functionality to have conversations with Home Assistant.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/conversation/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import warnings
|
||||
@@ -11,16 +12,17 @@ import warnings
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import script
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, HTTP_BAD_REQUEST)
|
||||
from homeassistant.helpers import intent, config_validation as cv
|
||||
from homeassistant.components import http
|
||||
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.15.0']
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.15.1']
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
ATTR_TEXT = 'text'
|
||||
ATTR_SENTENCE = 'sentence'
|
||||
DOMAIN = 'conversation'
|
||||
|
||||
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
||||
@@ -28,79 +30,174 @@ REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
||||
SERVICE_PROCESS = 'process'
|
||||
|
||||
SERVICE_PROCESS_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_TEXT): vol.All(cv.string, vol.Lower),
|
||||
vol.Required(ATTR_TEXT): cv.string,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({
|
||||
cv.string: vol.Schema({
|
||||
vol.Required(ATTR_SENTENCE): cv.string,
|
||||
vol.Required('action'): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional('intents'): vol.Schema({
|
||||
cv.string: vol.All(cv.ensure_list, [cv.string])
|
||||
})
|
||||
})}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def setup(hass, config):
|
||||
|
||||
@core.callback
|
||||
@bind_hass
|
||||
def async_register(hass, intent_type, utterances):
|
||||
"""Register an intent.
|
||||
|
||||
Registrations don't require conversations to be loaded. They will become
|
||||
active once the conversation component is loaded.
|
||||
"""
|
||||
intents = hass.data.get(DOMAIN)
|
||||
|
||||
if intents is None:
|
||||
intents = hass.data[DOMAIN] = {}
|
||||
|
||||
conf = intents.get(intent_type)
|
||||
|
||||
if conf is None:
|
||||
conf = intents[intent_type] = []
|
||||
|
||||
conf.extend(_create_matcher(utterance) for utterance in utterances)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Register the process service."""
|
||||
warnings.filterwarnings('ignore', module='fuzzywuzzy')
|
||||
from fuzzywuzzy import process as fuzzyExtract
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
config = config.get(DOMAIN, {})
|
||||
intents = hass.data.get(DOMAIN)
|
||||
|
||||
choices = {attrs[ATTR_SENTENCE]: script.Script(
|
||||
hass,
|
||||
attrs['action'],
|
||||
name)
|
||||
for name, attrs in config.items()}
|
||||
if intents is None:
|
||||
intents = hass.data[DOMAIN] = {}
|
||||
|
||||
for intent_type, utterances in config.get('intents', {}).items():
|
||||
conf = intents.get(intent_type)
|
||||
|
||||
if conf is None:
|
||||
conf = intents[intent_type] = []
|
||||
|
||||
conf.extend(_create_matcher(utterance) for utterance in utterances)
|
||||
|
||||
@asyncio.coroutine
|
||||
def process(service):
|
||||
"""Parse text into commands."""
|
||||
# if actually configured
|
||||
if choices:
|
||||
text = service.data[ATTR_TEXT]
|
||||
match = fuzzyExtract.extractOne(text, choices.keys())
|
||||
scorelimit = 60 # arbitrary value
|
||||
logging.info(
|
||||
'matched up text %s and found %s',
|
||||
text,
|
||||
[match[0] if match[1] > scorelimit else 'nothing']
|
||||
)
|
||||
if match[1] > scorelimit:
|
||||
choices[match[0]].run() # run respective script
|
||||
return
|
||||
|
||||
text = service.data[ATTR_TEXT]
|
||||
match = REGEX_TURN_COMMAND.match(text)
|
||||
yield from _process(hass, text)
|
||||
|
||||
if not match:
|
||||
logger.error("Unable to process: %s", text)
|
||||
return
|
||||
|
||||
name, command = match.groups()
|
||||
entities = {state.entity_id: state.name for state in hass.states.all()}
|
||||
entity_ids = fuzzyExtract.extractOne(
|
||||
name, entities, score_cutoff=65)[2]
|
||||
|
||||
if not entity_ids:
|
||||
logger.error(
|
||||
"Could not find entity id %s from text %s", name, text)
|
||||
return
|
||||
|
||||
if command == 'on':
|
||||
hass.services.call(core.DOMAIN, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity_ids,
|
||||
}, blocking=True)
|
||||
|
||||
elif command == 'off':
|
||||
hass.services.call(core.DOMAIN, SERVICE_TURN_OFF, {
|
||||
ATTR_ENTITY_ID: entity_ids,
|
||||
}, blocking=True)
|
||||
|
||||
else:
|
||||
logger.error('Got unsupported command %s from text %s',
|
||||
command, text)
|
||||
|
||||
hass.services.register(
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA)
|
||||
|
||||
hass.http.register_view(ConversationProcessView)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _create_matcher(utterance):
|
||||
"""Create a regex that matches the utterance."""
|
||||
parts = re.split(r'({\w+})', utterance)
|
||||
group_matcher = re.compile(r'{(\w+)}')
|
||||
|
||||
pattern = ['^']
|
||||
|
||||
for part in parts:
|
||||
match = group_matcher.match(part)
|
||||
|
||||
if match is None:
|
||||
pattern.append(part)
|
||||
continue
|
||||
|
||||
pattern.append('(?P<{}>{})'.format(match.groups()[0], r'[\w ]+'))
|
||||
|
||||
pattern.append('$')
|
||||
return re.compile(''.join(pattern), re.I)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _process(hass, text):
|
||||
"""Process a line of text."""
|
||||
intents = hass.data.get(DOMAIN, {})
|
||||
|
||||
for intent_type, matchers in intents.items():
|
||||
for matcher in matchers:
|
||||
match = matcher.match(text)
|
||||
|
||||
if not match:
|
||||
continue
|
||||
|
||||
response = yield from intent.async_handle(
|
||||
hass, DOMAIN, intent_type,
|
||||
{key: {'value': value} for key, value
|
||||
in match.groupdict().items()}, text)
|
||||
return response
|
||||
|
||||
from fuzzywuzzy import process as fuzzyExtract
|
||||
text = text.lower()
|
||||
match = REGEX_TURN_COMMAND.match(text)
|
||||
|
||||
if not match:
|
||||
_LOGGER.error("Unable to process: %s", text)
|
||||
return None
|
||||
|
||||
name, command = match.groups()
|
||||
entities = {state.entity_id: state.name for state
|
||||
in hass.states.async_all()}
|
||||
entity_ids = fuzzyExtract.extractOne(
|
||||
name, entities, score_cutoff=65)[2]
|
||||
|
||||
if not entity_ids:
|
||||
_LOGGER.error(
|
||||
"Could not find entity id %s from text %s", name, text)
|
||||
return None
|
||||
|
||||
if command == 'on':
|
||||
yield from hass.services.async_call(
|
||||
core.DOMAIN, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity_ids,
|
||||
}, blocking=True)
|
||||
|
||||
elif command == 'off':
|
||||
yield from hass.services.async_call(
|
||||
core.DOMAIN, SERVICE_TURN_OFF, {
|
||||
ATTR_ENTITY_ID: entity_ids,
|
||||
}, blocking=True)
|
||||
|
||||
else:
|
||||
_LOGGER.error('Got unsupported command %s from text %s',
|
||||
command, text)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class ConversationProcessView(http.HomeAssistantView):
|
||||
"""View to retrieve shopping list content."""
|
||||
|
||||
url = '/api/conversation/process'
|
||||
name = "api:conversation:process"
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Send a request for processing."""
|
||||
hass = request.app['hass']
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON specified',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
text = data.get('text')
|
||||
|
||||
if text is None:
|
||||
return self.json_message('Missing "text" key in JSON.',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
intent_result = yield from _process(hass, text)
|
||||
|
||||
if intent_result is None:
|
||||
intent_result = intent.IntentResponse()
|
||||
intent_result.async_set_speech("Sorry, I didn't understand that")
|
||||
|
||||
return self.json(intent_result)
|
||||
|
||||
@@ -13,6 +13,7 @@ import os
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
@@ -22,7 +23,7 @@ from homeassistant.const import (
|
||||
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
|
||||
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
|
||||
SERVICE_STOP_COVER_TILT, SERVICE_SET_COVER_TILT_POSITION, STATE_OPEN,
|
||||
STATE_CLOSED, STATE_UNKNOWN, ATTR_ENTITY_ID)
|
||||
STATE_CLOSED, STATE_UNKNOWN, STATE_OPENING, STATE_CLOSING, ATTR_ENTITY_ID)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -40,6 +41,8 @@ DEVICE_CLASSES = [
|
||||
'garage', # Garage door control
|
||||
]
|
||||
|
||||
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
||||
|
||||
SUPPORT_OPEN = 1
|
||||
SUPPORT_CLOSE = 2
|
||||
SUPPORT_SET_POSITION = 4
|
||||
@@ -84,24 +87,28 @@ SERVICE_TO_METHOD = {
|
||||
}
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_closed(hass, entity_id=None):
|
||||
"""Return if the cover is closed based on the statemachine."""
|
||||
entity_id = entity_id or ENTITY_ID_ALL_COVERS
|
||||
return hass.states.is_state(entity_id, STATE_CLOSED)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def open_cover(hass, entity_id=None):
|
||||
"""Open all or specified cover."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_OPEN_COVER, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def close_cover(hass, entity_id=None):
|
||||
"""Close all or specified cover."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_CLOSE_COVER, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_cover_position(hass, position, entity_id=None):
|
||||
"""Move to specific position all or specified cover."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
@@ -109,24 +116,28 @@ def set_cover_position(hass, position, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_COVER_POSITION, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def stop_cover(hass, entity_id=None):
|
||||
"""Stop all or specified cover."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_STOP_COVER, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def open_cover_tilt(hass, entity_id=None):
|
||||
"""Open all or specified cover tilt."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_OPEN_COVER_TILT, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def close_cover_tilt(hass, entity_id=None):
|
||||
"""Close all or specified cover tilt."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_CLOSE_COVER_TILT, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_cover_tilt_position(hass, tilt_position, entity_id=None):
|
||||
"""Move to specific tilt position all or specified cover."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
@@ -134,6 +145,7 @@ def set_cover_tilt_position(hass, tilt_position, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_COVER_TILT_POSITION, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def stop_cover_tilt(hass, entity_id=None):
|
||||
"""Stop all or specified cover tilt."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
@@ -213,6 +225,11 @@ class CoverDevice(Entity):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the cover."""
|
||||
if self.is_opening:
|
||||
return STATE_OPENING
|
||||
if self.is_closing:
|
||||
return STATE_CLOSING
|
||||
|
||||
closed = self.is_closed
|
||||
|
||||
if closed is None:
|
||||
@@ -250,6 +267,16 @@ class CoverDevice(Entity):
|
||||
|
||||
return supported_features
|
||||
|
||||
@property
|
||||
def is_opening(self):
|
||||
"""Return if the cover is opening or not."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def is_closing(self):
|
||||
"""Return if the cover is closing or not."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed or not."""
|
||||
|
||||
@@ -112,10 +112,7 @@ class CommandCover(CoverDevice):
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self.current_cover_position is not None:
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return self.current_cover_position == 0
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
|
||||
@@ -35,10 +35,12 @@ class DemoCover(CoverDevice):
|
||||
self._set_position = None
|
||||
self._set_tilt_position = None
|
||||
self._tilt_position = tilt_position
|
||||
self._closing = True
|
||||
self._closing_tilt = True
|
||||
self._requested_closing = True
|
||||
self._requested_closing_tilt = True
|
||||
self._unsub_listener_cover = None
|
||||
self._unsub_listener_cover_tilt = None
|
||||
self._is_opening = False
|
||||
self._is_closing = False
|
||||
if position is None:
|
||||
self._closed = True
|
||||
else:
|
||||
@@ -69,6 +71,16 @@ class DemoCover(CoverDevice):
|
||||
"""Return if the cover is closed."""
|
||||
return self._closed
|
||||
|
||||
@property
|
||||
def is_closing(self):
|
||||
"""Return if the cover is closing."""
|
||||
return self._is_closing
|
||||
|
||||
@property
|
||||
def is_opening(self):
|
||||
"""Return if the cover is opening."""
|
||||
return self._is_opening
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
@@ -79,8 +91,7 @@ class DemoCover(CoverDevice):
|
||||
"""Flag supported features."""
|
||||
if self._supported_features is not None:
|
||||
return self._supported_features
|
||||
else:
|
||||
return super().supported_features
|
||||
return super().supported_features
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
@@ -91,8 +102,10 @@ class DemoCover(CoverDevice):
|
||||
self.schedule_update_ha_state()
|
||||
return
|
||||
|
||||
self._is_closing = True
|
||||
self._listen_cover()
|
||||
self._closing = True
|
||||
self._requested_closing = True
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def close_cover_tilt(self, **kwargs):
|
||||
"""Close the cover tilt."""
|
||||
@@ -100,7 +113,7 @@ class DemoCover(CoverDevice):
|
||||
return
|
||||
|
||||
self._listen_cover_tilt()
|
||||
self._closing_tilt = True
|
||||
self._requested_closing_tilt = True
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
@@ -111,8 +124,10 @@ class DemoCover(CoverDevice):
|
||||
self.schedule_update_ha_state()
|
||||
return
|
||||
|
||||
self._is_opening = True
|
||||
self._listen_cover()
|
||||
self._closing = False
|
||||
self._requested_closing = False
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def open_cover_tilt(self, **kwargs):
|
||||
"""Open the cover tilt."""
|
||||
@@ -120,7 +135,7 @@ class DemoCover(CoverDevice):
|
||||
return
|
||||
|
||||
self._listen_cover_tilt()
|
||||
self._closing_tilt = False
|
||||
self._requested_closing_tilt = False
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
@@ -129,7 +144,7 @@ class DemoCover(CoverDevice):
|
||||
return
|
||||
|
||||
self._listen_cover()
|
||||
self._closing = position < self._position
|
||||
self._requested_closing = position < self._position
|
||||
|
||||
def set_cover_tilt_position(self, tilt_position, **kwargs):
|
||||
"""Move the cover til to a specific position."""
|
||||
@@ -138,10 +153,12 @@ class DemoCover(CoverDevice):
|
||||
return
|
||||
|
||||
self._listen_cover_tilt()
|
||||
self._closing_tilt = tilt_position < self._tilt_position
|
||||
self._requested_closing_tilt = tilt_position < self._tilt_position
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self._is_closing = False
|
||||
self._is_opening = False
|
||||
if self._position is None:
|
||||
return
|
||||
if self._unsub_listener_cover is not None:
|
||||
@@ -167,7 +184,7 @@ class DemoCover(CoverDevice):
|
||||
|
||||
def _time_changed_cover(self, now):
|
||||
"""Track time changes."""
|
||||
if self._closing:
|
||||
if self._requested_closing:
|
||||
self._position -= 10
|
||||
else:
|
||||
self._position += 10
|
||||
@@ -187,7 +204,7 @@ class DemoCover(CoverDevice):
|
||||
|
||||
def _time_changed_cover_tilt(self, now):
|
||||
"""Track time changes."""
|
||||
if self._closing_tilt:
|
||||
if self._requested_closing_tilt:
|
||||
self._tilt_position -= 10
|
||||
else:
|
||||
self._tilt_position += 10
|
||||
|
||||
@@ -159,8 +159,7 @@ class GaradgetCover(CoverDevice):
|
||||
"""Return if the cover is closed."""
|
||||
if self._state == STATE_UNKNOWN:
|
||||
return None
|
||||
else:
|
||||
return self._state == STATE_CLOSED
|
||||
return self._state == STATE_CLOSED
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
||||
@@ -20,8 +20,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMCover(hass, config)
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMCover(hass, conf)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
@@ -52,10 +52,7 @@ class HMCover(HMDevice, CoverDevice):
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self.current_cover_position is not None:
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return self.current_cover_position == 0
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
|
||||
185
homeassistant/components/cover/knx.py
Normal file
185
homeassistant/components/cover/knx.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
Support for KNX covers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.knx/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, PLATFORM_SCHEMA, ATTR_POSITION, DEVICE_CLASSES_SCHEMA,
|
||||
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION, SUPPORT_STOP,
|
||||
SUPPORT_SET_TILT_POSITION
|
||||
)
|
||||
from homeassistant.components.knx import (KNXConfig, KNXMultiAddressDevice)
|
||||
from homeassistant.const import (CONF_NAME, CONF_DEVICE_CLASS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_GETPOSITION_ADDRESS = 'getposition_address'
|
||||
CONF_SETPOSITION_ADDRESS = 'setposition_address'
|
||||
CONF_GETANGLE_ADDRESS = 'getangle_address'
|
||||
CONF_SETANGLE_ADDRESS = 'setangle_address'
|
||||
CONF_STOP = 'stop_address'
|
||||
CONF_UPDOWN = 'updown_address'
|
||||
CONF_INVERT_POSITION = 'invert_position'
|
||||
CONF_INVERT_ANGLE = 'invert_angle'
|
||||
|
||||
DEFAULT_NAME = 'KNX Cover'
|
||||
DEPENDENCIES = ['knx']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_UPDOWN): cv.string,
|
||||
vol.Required(CONF_STOP): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_GETPOSITION_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SETPOSITION_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
|
||||
vol.Inclusive(CONF_GETANGLE_ADDRESS, 'angle'): cv.string,
|
||||
vol.Inclusive(CONF_SETANGLE_ADDRESS, 'angle'): cv.string,
|
||||
vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Create and add an entity based on the configuration."""
|
||||
add_devices([KNXCover(hass, KNXConfig(config))])
|
||||
|
||||
|
||||
class KNXCover(KNXMultiAddressDevice, CoverDevice):
|
||||
"""Representation of a KNX cover. e.g. a rollershutter."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize the cover."""
|
||||
KNXMultiAddressDevice.__init__(
|
||||
self, hass, config,
|
||||
['updown', 'stop'], # required
|
||||
optional=['setposition', 'getposition',
|
||||
'getangle', 'setangle']
|
||||
)
|
||||
self._device_class = config.config.get(CONF_DEVICE_CLASS)
|
||||
self._invert_position = config.config.get(CONF_INVERT_POSITION)
|
||||
self._invert_angle = config.config.get(CONF_INVERT_ANGLE)
|
||||
self._hass = hass
|
||||
self._current_pos = None
|
||||
self._target_pos = None
|
||||
self._current_tilt = None
|
||||
self._target_tilt = None
|
||||
self._supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | \
|
||||
SUPPORT_SET_POSITION | SUPPORT_STOP
|
||||
|
||||
# Tilt is only supported, if there is a angle get and set address
|
||||
if CONF_SETANGLE_ADDRESS in config.config:
|
||||
_LOGGER.debug("%s: Tilt supported at addresses %s, %s",
|
||||
self.name, config.config.get(CONF_SETANGLE_ADDRESS),
|
||||
config.config.get(CONF_GETANGLE_ADDRESS))
|
||||
self._supported_features = self._supported_features | \
|
||||
SUPPORT_SET_TILT_POSITION
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling is needed for the KNX cover."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return self._supported_features
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self.current_cover_position is not None:
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return current position of cover.
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
return self._current_pos
|
||||
|
||||
@property
|
||||
def target_position(self):
|
||||
"""Return the position we are trying to reach: 0 - 100."""
|
||||
return self._target_pos
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self):
|
||||
"""Return current position of cover.
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
return self._current_tilt
|
||||
|
||||
@property
|
||||
def target_tilt(self):
|
||||
"""Return the tilt angle (in %) we are trying to reach: 0 - 100."""
|
||||
return self._target_tilt
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Set new target position."""
|
||||
position = kwargs.get(ATTR_POSITION)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
if self._invert_position:
|
||||
position = 100-position
|
||||
|
||||
self._target_pos = position
|
||||
self.set_percentage('setposition', position)
|
||||
_LOGGER.debug("%s: Set target position to %d", self.name, position)
|
||||
|
||||
def update(self):
|
||||
"""Update device state."""
|
||||
super().update()
|
||||
value = self.get_percentage('getposition')
|
||||
if value is not None:
|
||||
self._current_pos = value
|
||||
if self._invert_position:
|
||||
self._current_pos = 100-value
|
||||
_LOGGER.debug("%s: position = %d", self.name, value)
|
||||
|
||||
if self._supported_features & SUPPORT_SET_TILT_POSITION:
|
||||
value = self.get_percentage('getangle')
|
||||
if value is not None:
|
||||
self._current_tilt = value
|
||||
if self._invert_angle:
|
||||
self._current_tilt = 100-value
|
||||
_LOGGER.debug("%s: tilt = %d", self.name, value)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
_LOGGER.debug("%s: open: updown = 0", self.name)
|
||||
self.set_int_value('updown', 0)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
_LOGGER.debug("%s: open: updown = 1", self.name)
|
||||
self.set_int_value('updown', 1)
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover movement."""
|
||||
_LOGGER.debug("%s: stop: stop = 1", self.name)
|
||||
self.set_int_value('stop', 1)
|
||||
|
||||
def set_cover_tilt_position(self, tilt_position, **kwargs):
|
||||
"""Move the cover til to a specific position."""
|
||||
if self._invert_angle:
|
||||
tilt_position = 100-tilt_position
|
||||
|
||||
self._target_tilt = round(tilt_position, -1)
|
||||
self.set_percentage('setangle', tilt_position)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return self._device_class
|
||||
@@ -23,7 +23,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Lutron Caseta Serena shades as a cover device."""
|
||||
devs = []
|
||||
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
|
||||
cover_devices = bridge.get_devices_by_types(["SerenaRollerShade"])
|
||||
cover_devices = bridge.get_devices_by_types(["SerenaRollerShade",
|
||||
"SerenaHoneycombShade"])
|
||||
for cover_device in cover_devices:
|
||||
dev = LutronCasetaCover(cover_device, bridge)
|
||||
devs.append(dev)
|
||||
|
||||
@@ -361,8 +361,7 @@ class MqttCover(CoverDevice):
|
||||
position_percentage = float(offset_position) / tilt_range * 100.0
|
||||
if self._tilt_invert:
|
||||
return 100 - position_percentage
|
||||
else:
|
||||
return position_percentage
|
||||
return position_percentage
|
||||
|
||||
def find_in_range_from_percent(self, percentage):
|
||||
"""
|
||||
|
||||
@@ -12,7 +12,6 @@ from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, STATE_CLOSED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.loader as loader
|
||||
|
||||
REQUIREMENTS = ['pymyq==0.0.8']
|
||||
|
||||
@@ -37,7 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
brand = config.get(CONF_TYPE)
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
myq = pymyq(username, password, brand)
|
||||
|
||||
try:
|
||||
@@ -52,8 +50,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
except (TypeError, KeyError, NameError, ValueError) as ex:
|
||||
_LOGGER.error("%s", ex)
|
||||
persistent_notification.create(
|
||||
hass, 'Error: {}<br />'
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
|
||||
@@ -53,8 +53,7 @@ class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice):
|
||||
set_req = self.gateway.const.SetReq
|
||||
if set_req.V_DIMMER in self._values:
|
||||
return self._values.get(set_req.V_DIMMER) == 0
|
||||
else:
|
||||
return self._values.get(set_req.V_LIGHT) == STATE_OFF
|
||||
return self._values.get(set_req.V_LIGHT) == STATE_OFF
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
|
||||
@@ -117,8 +117,7 @@ class OpenGarageCover(CoverDevice):
|
||||
"""Return if the cover is closed."""
|
||||
if self._state == STATE_UNKNOWN:
|
||||
return None
|
||||
else:
|
||||
return self._state in [STATE_CLOSED, STATE_OPENING]
|
||||
return self._state in [STATE_CLOSED, STATE_OPENING]
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the cover."""
|
||||
|
||||
373
homeassistant/components/cover/template.py
Normal file
373
homeassistant/components/cover/template.py
Normal file
@@ -0,0 +1,373 @@
|
||||
"""
|
||||
Support for covers which integrate with other components.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.template/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.cover import (
|
||||
ENTITY_ID_FORMAT, CoverDevice, PLATFORM_SCHEMA,
|
||||
SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT,
|
||||
SUPPORT_SET_TILT_POSITION, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP,
|
||||
SUPPORT_SET_POSITION, ATTR_POSITION, ATTR_TILT_POSITION)
|
||||
from homeassistant.const import (
|
||||
CONF_FRIENDLY_NAME, CONF_ENTITY_ID,
|
||||
EVENT_HOMEASSISTANT_START, MATCH_ALL,
|
||||
CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE,
|
||||
STATE_OPEN, STATE_CLOSED)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.helpers.script import Script
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_VALID_STATES = [STATE_OPEN, STATE_CLOSED, 'true', 'false']
|
||||
|
||||
CONF_COVERS = 'covers'
|
||||
|
||||
CONF_POSITION_TEMPLATE = 'position_template'
|
||||
CONF_TILT_TEMPLATE = 'tilt_template'
|
||||
OPEN_ACTION = 'open_cover'
|
||||
CLOSE_ACTION = 'close_cover'
|
||||
STOP_ACTION = 'stop_cover'
|
||||
POSITION_ACTION = 'set_cover_position'
|
||||
TILT_ACTION = 'set_cover_tilt_position'
|
||||
CONF_VALUE_OR_POSITION_TEMPLATE = 'value_or_position'
|
||||
CONF_OPEN_OR_CLOSE = 'open_or_close'
|
||||
|
||||
TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT |
|
||||
SUPPORT_SET_TILT_POSITION)
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Inclusive(OPEN_ACTION, CONF_OPEN_OR_CLOSE): cv.SCRIPT_SCHEMA,
|
||||
vol.Inclusive(CLOSE_ACTION, CONF_OPEN_OR_CLOSE): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(STOP_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Exclusive(CONF_POSITION_TEMPLATE,
|
||||
CONF_VALUE_OR_POSITION_TEMPLATE): cv.template,
|
||||
vol.Exclusive(CONF_VALUE_TEMPLATE,
|
||||
CONF_VALUE_OR_POSITION_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_POSITION_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_TILT_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_ICON_TEMPLATE): cv.template,
|
||||
vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_FRIENDLY_NAME, default=None): cv.string,
|
||||
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Template cover."""
|
||||
covers = []
|
||||
|
||||
for device, device_config in config[CONF_COVERS].items():
|
||||
friendly_name = device_config.get(CONF_FRIENDLY_NAME, device)
|
||||
state_template = device_config.get(CONF_VALUE_TEMPLATE)
|
||||
position_template = device_config.get(CONF_POSITION_TEMPLATE)
|
||||
tilt_template = device_config.get(CONF_TILT_TEMPLATE)
|
||||
icon_template = device_config.get(CONF_ICON_TEMPLATE)
|
||||
open_action = device_config.get(OPEN_ACTION)
|
||||
close_action = device_config.get(CLOSE_ACTION)
|
||||
stop_action = device_config.get(STOP_ACTION)
|
||||
position_action = device_config.get(POSITION_ACTION)
|
||||
tilt_action = device_config.get(TILT_ACTION)
|
||||
|
||||
if position_template is None and state_template is None:
|
||||
_LOGGER.error('Must specify either %s' or '%s',
|
||||
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE)
|
||||
continue
|
||||
|
||||
if position_action is None and open_action is None:
|
||||
_LOGGER.error('Must specify at least one of %s' or '%s',
|
||||
OPEN_ACTION, POSITION_ACTION)
|
||||
continue
|
||||
template_entity_ids = set()
|
||||
if state_template is not None:
|
||||
temp_ids = state_template.extract_entities()
|
||||
if str(temp_ids) != MATCH_ALL:
|
||||
template_entity_ids |= set(temp_ids)
|
||||
|
||||
if position_template is not None:
|
||||
temp_ids = position_template.extract_entities()
|
||||
if str(temp_ids) != MATCH_ALL:
|
||||
template_entity_ids |= set(temp_ids)
|
||||
|
||||
if tilt_template is not None:
|
||||
temp_ids = tilt_template.extract_entities()
|
||||
if str(temp_ids) != MATCH_ALL:
|
||||
template_entity_ids |= set(temp_ids)
|
||||
|
||||
if icon_template is not None:
|
||||
temp_ids = icon_template.extract_entities()
|
||||
if str(temp_ids) != MATCH_ALL:
|
||||
template_entity_ids |= set(temp_ids)
|
||||
|
||||
if not template_entity_ids:
|
||||
template_entity_ids = MATCH_ALL
|
||||
|
||||
entity_ids = device_config.get(CONF_ENTITY_ID, template_entity_ids)
|
||||
|
||||
covers.append(
|
||||
CoverTemplate(
|
||||
hass,
|
||||
device, friendly_name, state_template,
|
||||
position_template, tilt_template, icon_template,
|
||||
open_action, close_action, stop_action,
|
||||
position_action, tilt_action, entity_ids
|
||||
)
|
||||
)
|
||||
if not covers:
|
||||
_LOGGER.error("No covers added")
|
||||
return False
|
||||
|
||||
async_add_devices(covers, True)
|
||||
return True
|
||||
|
||||
|
||||
class CoverTemplate(CoverDevice):
|
||||
"""Representation of a Template cover."""
|
||||
|
||||
def __init__(self, hass, device_id, friendly_name, state_template,
|
||||
position_template, tilt_template, icon_template,
|
||||
open_action, close_action, stop_action,
|
||||
position_action, tilt_action, entity_ids):
|
||||
"""Initialize the Template cover."""
|
||||
self.hass = hass
|
||||
self.entity_id = async_generate_entity_id(
|
||||
ENTITY_ID_FORMAT, device_id, hass=hass)
|
||||
self._name = friendly_name
|
||||
self._template = state_template
|
||||
self._position_template = position_template
|
||||
self._tilt_template = tilt_template
|
||||
self._icon_template = icon_template
|
||||
self._open_script = None
|
||||
if open_action is not None:
|
||||
self._open_script = Script(hass, open_action)
|
||||
self._close_script = None
|
||||
if close_action is not None:
|
||||
self._close_script = Script(hass, close_action)
|
||||
self._stop_script = None
|
||||
if stop_action is not None:
|
||||
self._stop_script = Script(hass, stop_action)
|
||||
self._position_script = None
|
||||
if position_action is not None:
|
||||
self._position_script = Script(hass, position_action)
|
||||
self._tilt_script = None
|
||||
if tilt_action is not None:
|
||||
self._tilt_script = Script(hass, tilt_action)
|
||||
self._icon = None
|
||||
self._position = None
|
||||
self._tilt_value = None
|
||||
self._entities = entity_ids
|
||||
|
||||
if self._template is not None:
|
||||
self._template.hass = self.hass
|
||||
if self._position_template is not None:
|
||||
self._position_template.hass = self.hass
|
||||
if self._tilt_template is not None:
|
||||
self._tilt_template.hass = self.hass
|
||||
if self._icon_template is not None:
|
||||
self._icon_template.hass = self.hass
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
if state:
|
||||
self._position = 100 if state.state == STATE_OPEN else 0
|
||||
|
||||
@callback
|
||||
def template_cover_state_listener(entity, old_state, new_state):
|
||||
"""Handle target device state changes."""
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
@callback
|
||||
def template_cover_startup(event):
|
||||
"""Update template on startup."""
|
||||
async_track_state_change(
|
||||
self.hass, self._entities, template_cover_state_listener)
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, template_cover_startup)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the cover."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
return self._position == 0
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return current position of cover.
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
return self._position
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self):
|
||||
"""Return current position of cover tilt.
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
return self._tilt_value
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
if self._stop_script is not None:
|
||||
supported_features |= SUPPORT_STOP
|
||||
|
||||
if self._position_script is not None:
|
||||
supported_features |= SUPPORT_SET_POSITION
|
||||
|
||||
if self.current_cover_tilt_position is not None:
|
||||
supported_features |= TILT_FEATURES
|
||||
|
||||
return supported_features
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_open_cover(self, **kwargs):
|
||||
"""Move the cover up."""
|
||||
if self._open_script:
|
||||
self.hass.async_add_job(self._open_script.async_run())
|
||||
elif self._position_script:
|
||||
self.hass.async_add_job(self._position_script.async_run(
|
||||
{"position": 100}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
"""Move the cover down."""
|
||||
if self._close_script:
|
||||
self.hass.async_add_job(self._close_script.async_run())
|
||||
elif self._position_script:
|
||||
self.hass.async_add_job(self._position_script.async_run(
|
||||
{"position": 0}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_stop_cover(self, **kwargs):
|
||||
"""Fire the stop action."""
|
||||
if self._stop_script:
|
||||
self.hass.async_add_job(self._stop_script.async_run())
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
"""Set cover position."""
|
||||
self._position = kwargs[ATTR_POSITION]
|
||||
self.hass.async_add_job(self._position_script.async_run(
|
||||
{"position": self._position}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_open_cover_tilt(self, **kwargs):
|
||||
"""Tilt the cover open."""
|
||||
self._tilt_value = 100
|
||||
self.hass.async_add_job(self._tilt_script.async_run(
|
||||
{"tilt": self._tilt_value}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover_tilt(self, **kwargs):
|
||||
"""Tilt the cover closed."""
|
||||
self._tilt_value = 0
|
||||
self.hass.async_add_job(self._tilt_script.async_run(
|
||||
{"tilt": self._tilt_value}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
self._tilt_value = kwargs[ATTR_TILT_POSITION]
|
||||
self.hass.async_add_job(self._tilt_script.async_run(
|
||||
{"tilt": self._tilt_value}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update the state from the template."""
|
||||
if self._template is not None:
|
||||
try:
|
||||
state = self._template.async_render().lower()
|
||||
if state in _VALID_STATES:
|
||||
if state in ('true', STATE_OPEN):
|
||||
self._position = 100
|
||||
else:
|
||||
self._position = 0
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'Received invalid cover is_on state: %s. Expected: %s',
|
||||
state, ', '.join(_VALID_STATES))
|
||||
self._position = None
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error(ex)
|
||||
self._position = None
|
||||
if self._position_template is not None:
|
||||
try:
|
||||
state = float(self._position_template.async_render())
|
||||
if state < 0 or state > 100:
|
||||
self._position = None
|
||||
_LOGGER.error("Cover position value must be"
|
||||
" between 0 and 100."
|
||||
" Value was: %.2f", state)
|
||||
else:
|
||||
self._position = state
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error(ex)
|
||||
self._position = None
|
||||
except ValueError as ex:
|
||||
_LOGGER.error(ex)
|
||||
self._position = None
|
||||
if self._tilt_template is not None:
|
||||
try:
|
||||
state = float(self._tilt_template.async_render())
|
||||
if state < 0 or state > 100:
|
||||
self._tilt_value = None
|
||||
_LOGGER.error("Tilt value must be between 0 and 100."
|
||||
" Value was: %.2f", state)
|
||||
else:
|
||||
self._tilt_value = state
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error(ex)
|
||||
self._tilt_value = None
|
||||
except ValueError as ex:
|
||||
_LOGGER.error(ex)
|
||||
self._tilt_value = None
|
||||
if self._icon_template is not None:
|
||||
try:
|
||||
self._icon = self._icon_template.async_render()
|
||||
except TemplateError as ex:
|
||||
if ex.args and ex.args[0].startswith(
|
||||
"UndefinedError: 'None' has no attribute"):
|
||||
# Common during HA startup - so just a warning
|
||||
_LOGGER.warning('Could not render icon template %s,'
|
||||
' the state is unknown.', self._name)
|
||||
return
|
||||
self._icon = super().icon
|
||||
_LOGGER.error('Could not render icon template %s: %s',
|
||||
self._name, ex)
|
||||
160
homeassistant/components/cover/velbus.py
Normal file
160
homeassistant/components/cover/velbus.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
Support for Velbus covers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.velbus/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE,
|
||||
SUPPORT_STOP)
|
||||
from homeassistant.components.velbus import DOMAIN
|
||||
from homeassistant.const import (CONF_COVERS, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Required('module'): cv.positive_int,
|
||||
vol.Required('open_channel'): cv.positive_int,
|
||||
vol.Required('close_channel'): cv.positive_int,
|
||||
vol.Required(CONF_NAME): cv.string
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
|
||||
})
|
||||
|
||||
DEPENDENCIES = ['velbus']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up cover controlled by Velbus."""
|
||||
devices = config.get(CONF_COVERS, {})
|
||||
covers = []
|
||||
|
||||
velbus = hass.data[DOMAIN]
|
||||
for device_name, device_config in devices.items():
|
||||
covers.append(
|
||||
VelbusCover(
|
||||
velbus,
|
||||
device_config.get(CONF_NAME, device_name),
|
||||
device_config.get('module'),
|
||||
device_config.get('open_channel'),
|
||||
device_config.get('close_channel')
|
||||
)
|
||||
)
|
||||
|
||||
if not covers:
|
||||
_LOGGER.error("No covers added")
|
||||
return False
|
||||
|
||||
add_devices(covers)
|
||||
|
||||
|
||||
class VelbusCover(CoverDevice):
|
||||
"""Representation a Velbus cover."""
|
||||
|
||||
def __init__(self, velbus, name, module, open_channel, close_channel):
|
||||
"""Initialize the cover."""
|
||||
self._velbus = velbus
|
||||
self._name = name
|
||||
self._close_channel_state = None
|
||||
self._open_channel_state = None
|
||||
self._module = module
|
||||
self._open_channel = open_channel
|
||||
self._close_channel = close_channel
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Add listener for Velbus messages on bus."""
|
||||
def _init_velbus():
|
||||
"""Initialize Velbus on startup."""
|
||||
self._velbus.subscribe(self._on_message)
|
||||
self.get_status()
|
||||
|
||||
yield from self.hass.async_add_job(_init_velbus)
|
||||
|
||||
def _on_message(self, message):
|
||||
import velbus
|
||||
if isinstance(message, velbus.RelayStatusMessage):
|
||||
if message.address == self._module:
|
||||
if message.channel == self._close_channel:
|
||||
self._close_channel_state = message.is_on()
|
||||
self.schedule_update_ha_state()
|
||||
if message.channel == self._open_channel:
|
||||
self._open_channel_state = message.is_on()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Disable polling."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the cover."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
return self._close_channel_state
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return current position of cover.
|
||||
|
||||
None is unknown.
|
||||
"""
|
||||
return None
|
||||
|
||||
def _relay_off(self, channel):
|
||||
import velbus
|
||||
message = velbus.SwitchRelayOffMessage()
|
||||
message.set_defaults(self._module)
|
||||
message.relay_channels = [channel]
|
||||
self._velbus.send(message)
|
||||
|
||||
def _relay_on(self, channel):
|
||||
import velbus
|
||||
message = velbus.SwitchRelayOnMessage()
|
||||
message.set_defaults(self._module)
|
||||
message.relay_channels = [channel]
|
||||
self._velbus.send(message)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self._relay_off(self._close_channel)
|
||||
time.sleep(0.3)
|
||||
self._relay_on(self._open_channel)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self._relay_off(self._open_channel)
|
||||
time.sleep(0.3)
|
||||
self._relay_on(self._close_channel)
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self._relay_off(self._open_channel)
|
||||
time.sleep(0.3)
|
||||
self._relay_off(self._close_channel)
|
||||
|
||||
def get_status(self):
|
||||
"""Retrieve current status."""
|
||||
import velbus
|
||||
message = velbus.ModuleStatusRequestMessage()
|
||||
message.set_defaults(self._module)
|
||||
message.channels = [self._open_channel, self._close_channel]
|
||||
self._velbus.send(message)
|
||||
@@ -53,10 +53,7 @@ class VeraCover(VeraDevice, CoverDevice):
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self.current_cover_position is not None:
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return self.current_cover_position == 0
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
|
||||
@@ -29,20 +29,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class WinkCoverDevice(WinkDevice, CoverDevice):
|
||||
"""Representation of a Wink cover device."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the cover."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
self.hass.data[DOMAIN]['entities']['cover'].append(self)
|
||||
|
||||
def close_cover(self):
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the shade."""
|
||||
self.wink.set_state(0)
|
||||
|
||||
def open_cover(self):
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the shade."""
|
||||
self.wink.set_state(1)
|
||||
|
||||
|
||||
66
homeassistant/components/cover/xiaomi.py
Normal file
66
homeassistant/components/cover/xiaomi.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""Support for Xiaomi curtain."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.xiaomi import (PY_XIAOMI_GATEWAY, XiaomiDevice)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_CURTAIN_LEVEL = 'curtain_level'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Perform the setup for Xiaomi devices."""
|
||||
devices = []
|
||||
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items():
|
||||
for device in gateway.devices['cover']:
|
||||
model = device['model']
|
||||
if model == 'curtain':
|
||||
devices.append(XiaomiGenericCover(device, "Curtain",
|
||||
{'status': 'status',
|
||||
'pos': 'curtain_level'},
|
||||
gateway))
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class XiaomiGenericCover(XiaomiDevice, CoverDevice):
|
||||
"""Representation of a XiaomiPlug."""
|
||||
|
||||
def __init__(self, device, name, data_key, xiaomi_hub):
|
||||
"""Initialize the XiaomiPlug."""
|
||||
self._data_key = data_key
|
||||
self._pos = 0
|
||||
XiaomiDevice.__init__(self, device, name, xiaomi_hub)
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return the current position of the cover."""
|
||||
return self._pos
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
return self.current_cover_position < 0
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self._write_to_hub(self._sid, self._data_key['status'], 'close')
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self._write_to_hub(self._sid, self._data_key['status'], 'open')
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self._write_to_hub(self._sid, self._data_key['status'], 'stop')
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
self._write_to_hub(self._sid, self._data_key['pos'], str(position))
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
if ATTR_CURTAIN_LEVEL in data:
|
||||
self._pos = int(data[ATTR_CURTAIN_LEVEL])
|
||||
return True
|
||||
return False
|
||||
@@ -27,10 +27,12 @@ def get_device(hass, values, node_config, **kwargs):
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
||||
and values.primary.index == 0):
|
||||
return ZwaveRollershutter(hass, values, invert_buttons)
|
||||
elif (values.primary.command_class in [
|
||||
zwave.const.COMMAND_CLASS_SWITCH_BINARY,
|
||||
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR]):
|
||||
return ZwaveGarageDoor(values)
|
||||
elif (values.primary.command_class ==
|
||||
zwave.const.COMMAND_CLASS_SWITCH_BINARY):
|
||||
return ZwaveGarageDoorSwitch(values)
|
||||
elif (values.primary.command_class ==
|
||||
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||
return ZwaveGarageDoorBarrier(values)
|
||||
return None
|
||||
|
||||
|
||||
@@ -73,8 +75,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
return None
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return True
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
@@ -86,8 +87,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
return 0
|
||||
elif self._current_position >= 95:
|
||||
return 100
|
||||
else:
|
||||
return self._current_position
|
||||
return self._current_position
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Move the roller shutter up."""
|
||||
@@ -106,17 +106,33 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
self._network.manager.releaseButton(self._open_id)
|
||||
|
||||
|
||||
class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Representation of an Zwave garage door device."""
|
||||
class ZwaveGarageDoorBase(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Base class for a Zwave garage door device."""
|
||||
|
||||
def __init__(self, values):
|
||||
"""Initialize the zwave garage door."""
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self._state = None
|
||||
self.update_properties()
|
||||
|
||||
def update_properties(self):
|
||||
"""Handle data changes for node values."""
|
||||
self._state = self.values.primary.data
|
||||
_LOGGER.debug("self._state=%s", self._state)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return 'garage'
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_GARAGE
|
||||
|
||||
|
||||
class ZwaveGarageDoorSwitch(ZwaveGarageDoorBase):
|
||||
"""Representation of a switch based Zwave garage door device."""
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
@@ -131,12 +147,29 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Open the garage door."""
|
||||
self.values.primary.data = True
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return 'garage'
|
||||
|
||||
class ZwaveGarageDoorBarrier(ZwaveGarageDoorBase):
|
||||
"""Representation of a barrier operator Zwave garage door device."""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_GARAGE
|
||||
def is_opening(self):
|
||||
"""Return true if cover is in an opening state."""
|
||||
return self._state == "Opening"
|
||||
|
||||
@property
|
||||
def is_closing(self):
|
||||
"""Return true if cover is in an closing state."""
|
||||
return self._state == "Closing"
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return the current position of Zwave garage door."""
|
||||
return self._state == "Closed"
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the garage door."""
|
||||
self.values.primary.data = "Closed"
|
||||
|
||||
def open_cover(self):
|
||||
"""Open the garage door."""
|
||||
self.values.primary.data = "Opened"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user