Compare commits
385 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c21172dd36 | |||
| 24bc035e22 | |||
| 36da5d9adb | |||
| d7d428119b | |||
| c458ee29f2 | |||
| 85a84549eb | |||
| 2465aea63b | |||
| e00e6f9db6 | |||
| 9fff634b9d | |||
| e7c157e766 | |||
| 9db1aa7629 | |||
| d998cba6a2 | |||
| 8013963784 | |||
| 7436a96978 | |||
| 3d9b2b5ed0 | |||
| 3b9fb6ccf5 | |||
| e3418f633c | |||
| bf3e5b460e | |||
| 03a6aa48e0 | |||
| 2aa996b558 | |||
| ef4a9bf354 | |||
| d4eabaf844 | |||
| 7ed83306ea | |||
| 2e7ae1d5fe | |||
| f2a42d767e | |||
| c3783bf49b | |||
| b817c7d0c2 | |||
| c2492d1493 | |||
| 5bba9a63a5 | |||
| d6747d6aaf | |||
| 0da8418f3f | |||
| 9f765836f8 | |||
| d58b901a78 | |||
| 394b52b9e8 | |||
| b67cce7215 | |||
| 0cf3c22da0 | |||
| a7cb9bdfff | |||
| 5f7d53c06b | |||
| 4b43537801 | |||
| 6abad6b76e | |||
| 6000c59bb5 | |||
| 2c3f55acc4 | |||
| a30711f1a0 | |||
| 1219ca3c3b | |||
| baa8e53e66 | |||
| f7a1d63d52 | |||
| d12decc471 | |||
| 64800fd48c | |||
| 9a3c0c8cd3 | |||
| d4a54acda0 | |||
| dc937cc8cf | |||
| eb9b95c292 | |||
| e68e29e03e | |||
| 1cf9ae5a01 | |||
| 3f3a3bcc8a | |||
| 467cb18625 | |||
| 82d037a828 | |||
| 34a9fb01ac | |||
| 3a4b4380a1 | |||
| 6b00f7ff28 | |||
| f75e13f55e | |||
| 6845a0974d | |||
| 7e1629a962 | |||
| 0b685a5b1e | |||
| 1f31dfe5d3 | |||
| 6be19e8997 | |||
| e6a9b6404f | |||
| dd7cafd5e3 | |||
| c7249a3e3a | |||
| bb02fc707c | |||
| 3ed7c1c6ad | |||
| ee055651cd | |||
| 9fdefa5a1d | |||
| f87016afe6 | |||
| b685e6e2b5 | |||
| 5983dc232f | |||
| 469472914b | |||
| a3971d7ad1 | |||
| 2b14d407c0 | |||
| 81f988cf9e | |||
| f643149d24 | |||
| fd50201407 | |||
| 469aad5fc8 | |||
| a65388e778 | |||
| 41ef6228be | |||
| ca4a857532 | |||
| 81922b88a2 | |||
| aa1e4c564c | |||
| 2b991e2f32 | |||
| 1719d88602 | |||
| c959637ebe | |||
| db623040a4 | |||
| ba29ba0fc3 | |||
| 93d462b010 | |||
| 50a8ec7335 | |||
| a36ca62445 | |||
| f88b5a9c5e | |||
| 51446e0772 | |||
| 95ddef31fe | |||
| 276a29c8f4 | |||
| 6b682d0d81 | |||
| cbda516af9 | |||
| cb85128304 | |||
| 74aa8194d7 | |||
| 497a1c84b5 | |||
| 2f907696f3 | |||
| 4ef7e08553 | |||
| ff0788324c | |||
| 9f65b8fef5 | |||
| 67ab1f69d8 | |||
| 5e8e2a8312 | |||
| 6ed3c69604 | |||
| d09dcc4b03 | |||
| 3f7a629079 | |||
| 8e61fab579 | |||
| d9614cff46 | |||
| 2a7a419ff3 | |||
| b78cf4772d | |||
| ebfb2c9b26 | |||
| e17ce4f374 | |||
| 67b74abf8d | |||
| c14a5fa7c1 | |||
| 2970196f61 | |||
| 4692ea85b7 | |||
| 018d786f36 | |||
| 254eb4e88a | |||
| 9eed03108f | |||
| fdd3fa7d80 | |||
| 6ec500f2e7 | |||
| a0a9f26312 | |||
| 21f59a0732 | |||
| 52f6fe3e06 | |||
| f71027a9c7 | |||
| 328ff6027b | |||
| c864ea60c9 | |||
| b2371c6614 | |||
| 9c6a985c56 | |||
| 5c006cd2d2 | |||
| a7cc7ce476 | |||
| 23f16bb68f | |||
| 8a463ef7a4 | |||
| 9af1e0ccf3 | |||
| a8a98f2585 | |||
| 7fbf68df35 | |||
| 01be70cda9 | |||
| e89aa6b2d6 | |||
| 227fb29cab | |||
| d04ee66669 | |||
| 9e66755baf | |||
| 0ecd185f0d | |||
| a2f17cccbb | |||
| 6892048de0 | |||
| aaff8d8602 | |||
| cf714f42df | |||
| f0b1874d2d | |||
| 98efbbc129 | |||
| d8ff22870a | |||
| fee47f35b9 | |||
| 7b6503c017 | |||
| c77b4a4806 | |||
| 4728fa8da6 | |||
| 68865ec27b | |||
| c5f70e8be3 | |||
| ec89accd29 | |||
| ac1063266c | |||
| 5b619a94ad | |||
| 244cdf43d0 | |||
| 22d1bf0acd | |||
| 43e5d28643 | |||
| e5dfcf7310 | |||
| 1c1b04718f | |||
| ce24ef0c20 | |||
| 308d71c448 | |||
| 203c1cfc96 | |||
| 6c50f53696 | |||
| 5e1e5992af | |||
| 9bf4a53fbb | |||
| 334b3b8636 | |||
| f18a88c2d4 | |||
| 9a16054867 | |||
| 35b4da0aa2 | |||
| 61fc4ca8fe | |||
| 4c9347eb2a | |||
| 25469dd8ee | |||
| b170f4c399 | |||
| a8b3900913 | |||
| 39bdd5310b | |||
| 133c03ee57 | |||
| f224ee7229 | |||
| 1aea3e0d51 | |||
| 877efac630 | |||
| ee6fb93018 | |||
| 08591aacc9 | |||
| 2cb67eca46 | |||
| 00b80d4fe1 | |||
| 53dde0e4e1 | |||
| 4778ec4f94 | |||
| 3ea984ca25 | |||
| 1258c4c680 | |||
| ed0d14c902 | |||
| 75dd391118 | |||
| 76a9eba744 | |||
| 31fe1d28e8 | |||
| a4a38c8a00 | |||
| 3b74cc606e | |||
| b750319de4 | |||
| 744d00a36e | |||
| a6d995e394 | |||
| f8af6e7863 | |||
| fec33347fb | |||
| 44eaca5985 | |||
| 18cf6f6f99 | |||
| 9f298a92f4 | |||
| 01e6bd2c92 | |||
| a76684f203 | |||
| 9bc16157af | |||
| 35d7f2b8bb | |||
| 7390f82e1f | |||
| cc9e5de503 | |||
| 50c8224365 | |||
| b08b376aa7 | |||
| 60ef0153a2 | |||
| 44c4b25f2b | |||
| 4abcaea4b7 | |||
| 831cad4220 | |||
| 6c524594c1 | |||
| 78f6cfd1eb | |||
| 6d6abab358 | |||
| 326cc83a17 | |||
| 8358ab56ea | |||
| 32dc518971 | |||
| b318a033bb | |||
| a0b2105ea0 | |||
| 9f9b87692a | |||
| 5c4f04e9fc | |||
| 757f6278eb | |||
| b9dcc2777b | |||
| 103fffa0f4 | |||
| 7748867732 | |||
| 02517ae5ec | |||
| 2a31bb48c6 | |||
| 5b70ada7b4 | |||
| 7b45cf8e59 | |||
| 394d53e748 | |||
| c125c4af4f | |||
| f90b89bc74 | |||
| ceac9eab94 | |||
| 7bb0abdf09 | |||
| 1d60760e21 | |||
| 43d18daebd | |||
| 1a7895b1d8 | |||
| c2f31bbb38 | |||
| a7e75dd01e | |||
| 58ea3c25df | |||
| 6d2de67620 | |||
| a359d21799 | |||
| be552a59c9 | |||
| 832f9737a8 | |||
| da6bdf275e | |||
| 7ca025f653 | |||
| 570cfc60c5 | |||
| dc551b825f | |||
| 6da3e23436 | |||
| e4b6395250 | |||
| 72bd9fb5c7 | |||
| 2dec38d8d4 | |||
| acb841a1f4 | |||
| eeb8bc3913 | |||
| 12f790c7cf | |||
| dbb4e4c3fa | |||
| d51e62d0a3 | |||
| ab92a91ac5 | |||
| cfa36f3546 | |||
| 96d8fbe513 | |||
| 1e9d91be0e | |||
| 2402897f47 | |||
| b857d5dad0 | |||
| d17753009a | |||
| 3467020dbf | |||
| 4114884cdc | |||
| d7ccf07922 | |||
| 2a7fa5afc3 | |||
| 04aa4e898a | |||
| b156ae7812 | |||
| 48928d1f9e | |||
| df98d5b3c1 | |||
| f4b5c439a1 | |||
| ecc514b7e4 | |||
| 6edb54052f | |||
| 4d2480bbd1 | |||
| 2708e193ec | |||
| c3923b2768 | |||
| 080c4efb00 | |||
| 99f1ea9b59 | |||
| 46cad514d4 | |||
| e0552ad899 | |||
| 5c99dd0e3d | |||
| cdf9464698 | |||
| 7ba25f3526 | |||
| ee5b9e7291 | |||
| 167260bcc6 | |||
| 64de1c9777 | |||
| 1547045f2c | |||
| d02899216d | |||
| 0aac4d64e1 | |||
| 0bf9e6d4bb | |||
| f78246e686 | |||
| c90a1b9760 | |||
| 14446c5731 | |||
| 2e2b764dbe | |||
| 695f062e29 | |||
| 194b268ae3 | |||
| 8295fc8b4c | |||
| d0dcd1bb73 | |||
| 82ad8b0a8f | |||
| 91a9da8f0c | |||
| e3415c4e22 | |||
| 9bca3f3103 | |||
| 7c3ae884df | |||
| 8a4aace789 | |||
| 0e74cd833d | |||
| 5e2911f071 | |||
| 7dacc4a7bb | |||
| 98fe50d5ad | |||
| 37e3c2a133 | |||
| 76b79019ce | |||
| c40ddf18c7 | |||
| 9a3fe691b1 | |||
| 8826e6a8d0 | |||
| 860a12cffb | |||
| 76ff934bd3 | |||
| d968e1d011 | |||
| fa0dbaf065 | |||
| 4d0f19496a | |||
| 0cc9555d14 | |||
| d712a3dc38 | |||
| 84446bed14 | |||
| e92b15f966 | |||
| a458ce8069 | |||
| 5e492db9a3 | |||
| bc646070c8 | |||
| 64290d74f0 | |||
| a11b68c560 | |||
| 8ca2345fd4 | |||
| 8c628071f3 | |||
| 81d38c3463 | |||
| 776455030f | |||
| 8afd30b7d4 | |||
| b60f5714fc | |||
| fa8bc0a36c | |||
| 1ae8256ffd | |||
| b3253403aa | |||
| 308744d8a0 | |||
| 13006cee68 | |||
| e21382cd3e | |||
| 71fc446425 | |||
| 03d19ec2f1 | |||
| 5a7e446646 | |||
| 2b3caa716a | |||
| 6574dd8439 | |||
| 2099d023ef | |||
| 64b1179c13 | |||
| 87dab37b8a | |||
| cffc7ac4d8 | |||
| a9be6c36f1 | |||
| 1b35f0878e | |||
| 93872590b6 | |||
| b2a15e17d3 | |||
| 9bf13231f7 | |||
| c8c6bee539 | |||
| b5c2be8ffa | |||
| 4d35f2805f | |||
| 53c1b93b61 | |||
| c25aa56751 | |||
| e8c9dcf0fe | |||
| ca63e44227 | |||
| 776e53a7f0 | |||
| a099430834 | |||
| 7746ecd98e | |||
| 10d1496f5a | |||
| cf0ff54d14 | |||
| 97cc76b43e | |||
| c89e6ec915 | |||
| efdf51b542 | |||
| bbb251c0cf |
+33
-1
@@ -13,6 +13,9 @@ omit =
|
||||
homeassistant/components/arduino.py
|
||||
homeassistant/components/*/arduino.py
|
||||
|
||||
homeassistant/components/bbb_gpio.py
|
||||
homeassistant/components/*/bbb_gpio.py
|
||||
|
||||
homeassistant/components/bloomsky.py
|
||||
homeassistant/components/*/bloomsky.py
|
||||
|
||||
@@ -34,6 +37,9 @@ omit =
|
||||
homeassistant/components/insteon_hub.py
|
||||
homeassistant/components/*/insteon_hub.py
|
||||
|
||||
homeassistant/components/insteon_local.py
|
||||
homeassistant/components/*/insteon_local.py
|
||||
|
||||
homeassistant/components/ios.py
|
||||
homeassistant/components/*/ios.py
|
||||
|
||||
@@ -122,6 +128,8 @@ omit =
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
homeassistant/components/binary_sensor/concord232.py
|
||||
homeassistant/components/binary_sensor/flic.py
|
||||
homeassistant/components/binary_sensor/hikvision.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/amcrest.py
|
||||
@@ -155,16 +163,20 @@ omit =
|
||||
homeassistant/components/device_tracker/luci.py
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
homeassistant/components/device_tracker/nmap_tracker.py
|
||||
homeassistant/components/device_tracker/ping.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
homeassistant/components/device_tracker/swisscom.py
|
||||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/trackr.py
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
homeassistant/components/device_tracker/volvooncall.py
|
||||
homeassistant/components/device_tracker/xiaomi.py
|
||||
homeassistant/components/discovery.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
|
||||
@@ -180,9 +192,12 @@ omit =
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/light/tikteck.py
|
||||
homeassistant/components/light/x10.py
|
||||
homeassistant/components/light/yeelight.py
|
||||
homeassistant/components/light/zengge.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/media_player/aquostv.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/cmus.py
|
||||
@@ -198,6 +213,7 @@ omit =
|
||||
homeassistant/components/media_player/lg_netcast.py
|
||||
homeassistant/components/media_player/mpchc.py
|
||||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/media_player/nad.py
|
||||
homeassistant/components/media_player/onkyo.py
|
||||
homeassistant/components/media_player/panasonic_viera.py
|
||||
homeassistant/components/media_player/pandora.py
|
||||
@@ -210,16 +226,19 @@ omit =
|
||||
homeassistant/components/media_player/snapcast.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
homeassistant/components/notify/aws_sqs.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
|
||||
homeassistant/components/notify/matrix.py
|
||||
homeassistant/components/notify/message_bird.py
|
||||
@@ -248,6 +267,8 @@ omit =
|
||||
homeassistant/components/sensor/bbox.py
|
||||
homeassistant/components/sensor/bitcoin.py
|
||||
homeassistant/components/sensor/bom.py
|
||||
homeassistant/components/sensor/broadlink.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/cups.py
|
||||
@@ -271,6 +292,8 @@ omit =
|
||||
homeassistant/components/sensor/haveibeenpwned.py
|
||||
homeassistant/components/sensor/hddtemp.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/hydroquebec.py
|
||||
homeassistant/components/sensor/iss.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
@@ -280,6 +303,7 @@ omit =
|
||||
homeassistant/components/sensor/mhz19.py
|
||||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/mqtt_room.py
|
||||
homeassistant/components/sensor/netdata.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/nut.py
|
||||
homeassistant/components/sensor/nzbget.py
|
||||
@@ -292,7 +316,9 @@ omit =
|
||||
homeassistant/components/sensor/pvoutput.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/sma.py
|
||||
homeassistant/components/sensor/snmp.py
|
||||
homeassistant/components/sensor/sonarr.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
@@ -309,18 +335,22 @@ omit =
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/usps.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
homeassistant/components/sensor/yweather.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
homeassistant/components/sensor/zamg.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
homeassistant/components/switch/anel_pwrctrl.py
|
||||
homeassistant/components/switch/arest.py
|
||||
homeassistant/components/switch/broadlink.py
|
||||
homeassistant/components/switch/digitalloggers.py
|
||||
homeassistant/components/switch/dlink.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
homeassistant/components/switch/hook.py
|
||||
homeassistant/components/switch/kankun.py
|
||||
homeassistant/components/switch/mystrom.py
|
||||
homeassistant/components/switch/netio.py
|
||||
homeassistant/components/switch/orvibo.py
|
||||
@@ -332,7 +362,9 @@ omit =
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/picotts.py
|
||||
homeassistant/components/upnp.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/openweathermap.py
|
||||
homeassistant/components/zeroconf.py
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ matrix:
|
||||
env: TOXENV=typing
|
||||
- python: "3.5"
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
allow_failures:
|
||||
- python: "3.5"
|
||||
env: TOXENV=typing
|
||||
|
||||
+3
-2
@@ -1,13 +1,14 @@
|
||||
# Contributing to Home Assistant
|
||||
|
||||
Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them?
|
||||
Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them?
|
||||
|
||||
The process is straight-forward.
|
||||
|
||||
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/devel/faster_reviews.md) by Kubernetes (but skip step 0)
|
||||
- Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant).
|
||||
- Write the code for your device, notification service, sensor, or IoT thing.
|
||||
- Ensure tests work.
|
||||
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
|
||||
|
||||
Still interested? Then you should take a peak at the [developer documentation](https://home-assistant.io/developers/) to get more details.
|
||||
Still interested? Then you should take a peak at the [developer documentation](https://home-assistant.io/developers/) to get more details.
|
||||
|
||||
|
||||
+5
-12
@@ -6,21 +6,14 @@ VOLUME /config
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN pip3 install --no-cache-dir colorlog cython
|
||||
|
||||
# For the nmap tracker, bluetooth tracker, Z-Wave
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends nmap net-tools cython3 libudev-dev sudo libglib2.0-dev bluetooth libbluetooth-dev && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
COPY script/build_python_openzwave script/build_python_openzwave
|
||||
RUN script/build_python_openzwave && \
|
||||
mkdir -p /usr/local/share/python-openzwave && \
|
||||
ln -sf /usr/src/app/build/python-openzwave/openzwave/config /usr/local/share/python-openzwave/config
|
||||
# Copy build scripts
|
||||
COPY script/setup_docker_prereqs script/build_python_openzwave script/build_libcec script/
|
||||
RUN script/setup_docker_prereqs
|
||||
|
||||
# Install hass component dependencies
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
|
||||
pip3 install mysqlclient psycopg2 uvloop
|
||||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
+2
-1
@@ -26,7 +26,8 @@ Examples of devices Home Assistant can interface with:
|
||||
`Netgear <http://netgear.com>`__,
|
||||
`DD-WRT <http://www.dd-wrt.com/site/index>`__,
|
||||
`TPLink <http://www.tp-link.us/>`__,
|
||||
`ASUSWRT <http://event.asus.com/2013/nw/ASUSWRT/>`__ and any SNMP
|
||||
`ASUSWRT <http://event.asus.com/2013/nw/ASUSWRT/>`__,
|
||||
`Xiaomi <http://miwifi.com/>`__ and any SNMP
|
||||
capable Linksys WAP/WRT
|
||||
- `Philips Hue <http://meethue.com>`__ lights,
|
||||
`WeMo <http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/>`__
|
||||
|
||||
@@ -356,7 +356,8 @@ def try_to_restart() -> None:
|
||||
|
||||
def main() -> int:
|
||||
"""Start Home Assistant."""
|
||||
monkey_patch_asyncio()
|
||||
if sys.version_info[:3] < (3, 5, 3):
|
||||
monkey_patch_asyncio()
|
||||
|
||||
validate_python()
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import homeassistant.loader as loader
|
||||
import homeassistant.util.package as pkg_util
|
||||
from homeassistant.util.async import (
|
||||
run_coroutine_threadsafe, run_callback_threadsafe)
|
||||
from homeassistant.util.logging import AsyncHandler
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -394,6 +395,10 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
if not loader.PREPARED:
|
||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||
|
||||
# Merge packages
|
||||
conf_util.merge_packages_config(
|
||||
config, core_config.get(conf_util.CONF_PACKAGES, {}))
|
||||
|
||||
# Make a copy because we are mutating it.
|
||||
# Use OrderedDict in case original one was one.
|
||||
# Convert values to dictionaries if they are None
|
||||
@@ -528,6 +533,10 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# AsyncHandler allready exists?
|
||||
if hass.data.get(core.DATA_ASYNCHANDLER):
|
||||
return
|
||||
|
||||
# Log errors to a file if we have write access to file or config dir
|
||||
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||
err_path_exists = os.path.isfile(err_log_path)
|
||||
@@ -548,8 +557,12 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
err_handler.setFormatter(
|
||||
logging.Formatter('%(asctime)s %(name)s: %(message)s',
|
||||
datefmt='%y-%m-%d %H:%M:%S'))
|
||||
|
||||
async_handler = AsyncHandler(hass.loop, err_handler)
|
||||
hass.data[core.DATA_ASYNCHANDLER] = async_handler
|
||||
|
||||
logger = logging.getLogger('')
|
||||
logger.addHandler(err_handler)
|
||||
logger.addHandler(async_handler)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
else:
|
||||
@@ -597,7 +610,7 @@ def async_log_exception(ex, domain, config, hass):
|
||||
message += '{}.'.format(humanize_error(config, ex))
|
||||
|
||||
domain_config = config.get(domain, config)
|
||||
message += " (See {}:{}). ".format(
|
||||
message += " (See {}, line {}). ".format(
|
||||
getattr(domain_config, '__config_file__', '?'),
|
||||
getattr(domain_config, '__line__', '?'))
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ Component to interface with an alarm control panel.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -19,7 +21,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
DOMAIN = 'alarm_control_panel'
|
||||
SCAN_INTERVAL = 30
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
ATTR_CHANGED_BY = 'changed_by'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
@@ -42,36 +44,6 @@ ALARM_SERVICE_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for sensors."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
def alarm_service_handler(service):
|
||||
"""Map services to methods on Alarm."""
|
||||
target_alarms = component.extract_from_service(service)
|
||||
|
||||
code = service.data.get(ATTR_CODE)
|
||||
|
||||
method = SERVICE_TO_METHOD[service.service]
|
||||
|
||||
for alarm in target_alarms:
|
||||
getattr(alarm, method)(code)
|
||||
if alarm.should_poll:
|
||||
alarm.update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
for service in SERVICE_TO_METHOD:
|
||||
hass.services.register(DOMAIN, service, alarm_service_handler,
|
||||
descriptions.get(service),
|
||||
schema=ALARM_SERVICE_SCHEMA)
|
||||
return True
|
||||
|
||||
|
||||
def alarm_disarm(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for disarm."""
|
||||
data = {}
|
||||
@@ -116,6 +88,53 @@ def alarm_trigger(hass, code=None, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Track states and offer events for sensors."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_service_handler(service):
|
||||
"""Map services to methods on Alarm."""
|
||||
target_alarms = component.async_extract_from_service(service)
|
||||
|
||||
code = service.data.get(ATTR_CODE)
|
||||
|
||||
method = "async_{}".format(SERVICE_TO_METHOD[service.service])
|
||||
|
||||
for alarm in target_alarms:
|
||||
yield from getattr(alarm, method)(code)
|
||||
|
||||
update_tasks = []
|
||||
for alarm in target_alarms:
|
||||
if not alarm.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
alarm.async_update_ha_state(True))
|
||||
if hasattr(alarm, '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.loop.run_in_executor(
|
||||
None, load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
for service in SERVICE_TO_METHOD:
|
||||
hass.services.async_register(
|
||||
DOMAIN, service, async_alarm_service_handler,
|
||||
descriptions.get(service), schema=ALARM_SERVICE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class AlarmControlPanel(Entity):
|
||||
"""An abstract class for alarm control devices."""
|
||||
@@ -134,18 +153,50 @@ class AlarmControlPanel(Entity):
|
||||
"""Send disarm command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.alarm_disarm, code)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.alarm_arm_home, code)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.alarm_arm_away, code)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.alarm_trigger, code)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
|
||||
@@ -56,11 +56,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
self._password = password
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return True
|
||||
|
||||
def update(self):
|
||||
"""Fetch the latest state."""
|
||||
self._state = self._alarm.state
|
||||
|
||||
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.concord232/
|
||||
"""
|
||||
import datetime
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import requests
|
||||
@@ -25,7 +26,7 @@ DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_NAME = 'CONCORD232'
|
||||
DEFAULT_PORT = 5007
|
||||
|
||||
SCAN_INTERVAL = 1
|
||||
SCAN_INTERVAL = timedelta(seconds=1)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
@@ -71,11 +72,6 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
self._alarm.last_partition_update = datetime.datetime.now()
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling needed."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
@@ -126,7 +122,3 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._alarm.arm('auto')
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -97,7 +97,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
def _update_callback(self, partition):
|
||||
"""Update HA state, if needed."""
|
||||
if partition is None or int(partition) == self._partition_number:
|
||||
self.hass.async_add_job(self.update_ha_state)
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
|
||||
@@ -116,7 +116,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
@@ -125,7 +125,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
@@ -139,7 +139,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
@@ -151,7 +151,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
self._pre_trigger_state = self._state
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._trigger_time:
|
||||
track_point_in_time(
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN, CONF_NAME, CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pynx584==0.2']
|
||||
REQUIREMENTS = ['pynx584==0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -62,11 +62,6 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
self._alarm.list_zones()
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling needed."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
@@ -91,9 +86,11 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
_LOGGER.error('Unable to connect to %(host)s: %(reason)s',
|
||||
dict(host=self._url, reason=ex))
|
||||
self._state = STATE_UNKNOWN
|
||||
zones = []
|
||||
except IndexError:
|
||||
_LOGGER.error('nx584 reports no partitions')
|
||||
self._state = STATE_UNKNOWN
|
||||
zones = []
|
||||
|
||||
bypassed = False
|
||||
for zone in zones:
|
||||
@@ -122,7 +119,3 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._alarm.arm('exit')
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -61,11 +61,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
else:
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Poll the SimpliSafe API."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
@@ -104,7 +99,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
return
|
||||
self.simplisafe.set_state('off')
|
||||
_LOGGER.info('SimpliSafe alarm disarming')
|
||||
self.update()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
@@ -112,7 +106,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
return
|
||||
self.simplisafe.set_state('home')
|
||||
_LOGGER.info('SimpliSafe alarm arming home')
|
||||
self.update()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
@@ -120,7 +113,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
return
|
||||
self.simplisafe.set_state('away')
|
||||
_LOGGER.info('SimpliSafe alarm arming away')
|
||||
self.update()
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
|
||||
@@ -84,18 +84,15 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
hub.my_pages.alarm.set(code, 'DISARMED')
|
||||
_LOGGER.info('verisure alarm disarming')
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
self.update()
|
||||
|
||||
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()
|
||||
self.update()
|
||||
|
||||
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()
|
||||
self.update()
|
||||
|
||||
@@ -203,11 +203,12 @@ class AlexaResponse(object):
|
||||
self.reprompt = None
|
||||
self.session_attributes = {}
|
||||
self.should_end_session = True
|
||||
self.variables = {}
|
||||
if intent is not None and 'slots' in intent:
|
||||
self.variables = {key: value['value'] for key, value
|
||||
in intent['slots'].items() if 'value' in value}
|
||||
else:
|
||||
self.variables = {}
|
||||
for key, value in intent['slots'].items():
|
||||
if 'value' in value:
|
||||
underscored_key = key.replace('.', '_')
|
||||
self.variables[underscored_key] = value['value']
|
||||
|
||||
def add_card(self, card_type, title, content):
|
||||
"""Add a card to the response."""
|
||||
|
||||
@@ -133,6 +133,9 @@ class APIEventStream(HomeAssistantView):
|
||||
except asyncio.TimeoutError:
|
||||
yield from to_write.put(STREAM_PING_PAYLOAD)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug('STREAM %s ABORT', id(stop_obj))
|
||||
|
||||
finally:
|
||||
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
|
||||
unsub_stream()
|
||||
|
||||
@@ -166,7 +166,9 @@ def async_setup(hass, config):
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
tasks.append(entity.async_trigger(
|
||||
service_call.data.get(ATTR_VARIABLES), True))
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def turn_onoff_service_handler(service_call):
|
||||
@@ -175,7 +177,9 @@ def async_setup(hass, config):
|
||||
method = 'async_{}'.format(service_call.service)
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
tasks.append(getattr(entity, method)())
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def toggle_service_handler(service_call):
|
||||
@@ -186,7 +190,9 @@ def async_setup(hass, config):
|
||||
tasks.append(entity.async_turn_off())
|
||||
else:
|
||||
tasks.append(entity.async_turn_on())
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def reload_service_handler(service_call):
|
||||
|
||||
@@ -4,6 +4,8 @@ Offer MQTT listening automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#mqtt-trigger
|
||||
"""
|
||||
import json
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
@@ -31,13 +33,20 @@ def async_trigger(hass, config, action):
|
||||
def mqtt_automation_listener(msg_topic, msg_payload, qos):
|
||||
"""Listen for MQTT messages."""
|
||||
if payload is None or payload == msg_payload:
|
||||
data = {
|
||||
'platform': 'mqtt',
|
||||
'topic': msg_topic,
|
||||
'payload': msg_payload,
|
||||
'qos': qos,
|
||||
}
|
||||
|
||||
try:
|
||||
data['payload_json'] = json.loads(msg_payload)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'mqtt',
|
||||
'topic': msg_topic,
|
||||
'payload': msg_payload,
|
||||
'qos': qos,
|
||||
}
|
||||
'trigger': data
|
||||
})
|
||||
|
||||
return mqtt.async_subscribe(hass, topic, mqtt_automation_listener)
|
||||
|
||||
@@ -64,10 +64,19 @@ def async_trigger(hass, config, action):
|
||||
call_action()
|
||||
return
|
||||
|
||||
@callback
|
||||
def clear_listener():
|
||||
"""Clear all unsub listener."""
|
||||
nonlocal async_remove_state_for_cancel
|
||||
nonlocal async_remove_state_for_listener
|
||||
async_remove_state_for_listener = None
|
||||
async_remove_state_for_cancel = None
|
||||
|
||||
@callback
|
||||
def state_for_listener(now):
|
||||
"""Fire on state changes after a delay and calls action."""
|
||||
async_remove_state_for_cancel()
|
||||
clear_listener()
|
||||
call_action()
|
||||
|
||||
@callback
|
||||
@@ -77,6 +86,7 @@ def async_trigger(hass, config, action):
|
||||
return
|
||||
async_remove_state_for_listener()
|
||||
async_remove_state_for_cancel()
|
||||
clear_listener()
|
||||
|
||||
async_remove_state_for_listener = async_track_point_in_utc_time(
|
||||
hass, state_for_listener, dt_util.utcnow() + time_delta)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Support for controlling GPIO pins of a Beaglebone Black.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/bbb_gpio/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
REQUIREMENTS = ['Adafruit_BBIO==1.0.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'bbb_gpio'
|
||||
|
||||
|
||||
# pylint: disable=no-member
|
||||
def setup(hass, config):
|
||||
"""Set up the BeagleBone Black GPIO component."""
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
|
||||
def cleanup_gpio(event):
|
||||
"""Stuff to do before stopping."""
|
||||
GPIO.cleanup()
|
||||
|
||||
def prepare_gpio(event):
|
||||
"""Stuff to do when home assistant starts."""
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio)
|
||||
return True
|
||||
|
||||
|
||||
# noqa: F821
|
||||
|
||||
def setup_output(pin):
|
||||
"""Setup a GPIO as output."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
GPIO.setup(pin, GPIO.OUT)
|
||||
|
||||
|
||||
def setup_input(pin, pull_mode):
|
||||
"""Setup a GPIO as input."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
GPIO.setup(pin, GPIO.IN, # noqa: F821
|
||||
GPIO.PUD_DOWN if pull_mode == 'DOWN' # noqa: F821
|
||||
else GPIO.PUD_UP) # noqa: F821
|
||||
|
||||
|
||||
def write_output(pin, value):
|
||||
"""Write a value to a GPIO."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
GPIO.output(pin, value)
|
||||
|
||||
|
||||
def read_input(pin):
|
||||
"""Read a value from a GPIO."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
return GPIO.input(pin)
|
||||
|
||||
|
||||
def edge_detect(pin, event_callback, bounce):
|
||||
"""Add detection for RISING and FALLING events."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
GPIO.add_event_detect(
|
||||
pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce)
|
||||
@@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -15,7 +16,7 @@ from homeassistant.const import (STATE_ON, STATE_OFF)
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
|
||||
DOMAIN = 'binary_sensor'
|
||||
SCAN_INTERVAL = 30
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
SENSOR_CLASSES = [
|
||||
|
||||
@@ -4,6 +4,7 @@ Support for custom shell commands to retrieve values.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.command_line/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -22,7 +23,7 @@ DEFAULT_NAME = 'Binary Command Sensor'
|
||||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
|
||||
SCAN_INTERVAL = 60
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COMMAND): cv.string,
|
||||
|
||||
@@ -27,7 +27,7 @@ DEFAULT_NAME = 'Alarm'
|
||||
DEFAULT_PORT = '5007'
|
||||
DEFAULT_SSL = False
|
||||
|
||||
SCAN_INTERVAL = 1
|
||||
SCAN_INTERVAL = datetime.timedelta(seconds=1)
|
||||
|
||||
ZONE_TYPES_SCHEMA = vol.Schema({
|
||||
cv.positive_int: vol.In(SENSOR_CLASSES),
|
||||
|
||||
@@ -20,7 +20,7 @@ DEPENDENCIES = ['enocean']
|
||||
DEFAULT_NAME = 'EnOcean binary sensor'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ID): cv.string,
|
||||
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
"""Contains functionality to use flic buttons as a binary sensor."""
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PORT, CONF_DISCOVERY, CONF_TIMEOUT,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
|
||||
|
||||
REQUIREMENTS = ['https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_TIMEOUT = 3
|
||||
|
||||
CLICK_TYPE_SINGLE = "single"
|
||||
CLICK_TYPE_DOUBLE = "double"
|
||||
CLICK_TYPE_HOLD = "hold"
|
||||
CLICK_TYPES = [CLICK_TYPE_SINGLE, CLICK_TYPE_DOUBLE, CLICK_TYPE_HOLD]
|
||||
|
||||
CONF_IGNORED_CLICK_TYPES = "ignored_click_types"
|
||||
|
||||
EVENT_NAME = "flic_click"
|
||||
EVENT_DATA_NAME = "button_name"
|
||||
EVENT_DATA_ADDRESS = "button_address"
|
||||
EVENT_DATA_TYPE = "click_type"
|
||||
EVENT_DATA_QUEUED_TIME = "queued_time"
|
||||
|
||||
# Validation of the user's configuration
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST, default='localhost'): cv.string,
|
||||
vol.Optional(CONF_PORT, default=5551): cv.port,
|
||||
vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_IGNORED_CLICK_TYPES): vol.All(cv.ensure_list,
|
||||
[vol.In(CLICK_TYPES)])
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Setup the flic platform."""
|
||||
import pyflic
|
||||
|
||||
# Initialize flic client responsible for
|
||||
# connecting to buttons and retrieving events
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
discovery = config.get(CONF_DISCOVERY)
|
||||
|
||||
try:
|
||||
client = pyflic.FlicClient(host, port)
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.error("Failed to connect to flic server.")
|
||||
return
|
||||
|
||||
def new_button_callback(address):
|
||||
"""Setup newly verified button as device in home assistant."""
|
||||
setup_button(hass, config, add_entities, client, address)
|
||||
|
||||
client.on_new_verified_button = new_button_callback
|
||||
if discovery:
|
||||
start_scanning(config, add_entities, client)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
lambda event: client.close())
|
||||
|
||||
# Start the pyflic event handling thread
|
||||
threading.Thread(target=client.handle_events).start()
|
||||
|
||||
def get_info_callback(items):
|
||||
"""Add entities for already verified buttons."""
|
||||
addresses = items["bd_addr_of_verified_buttons"] or []
|
||||
for address in addresses:
|
||||
setup_button(hass, config, add_entities, client, address)
|
||||
|
||||
# Get addresses of already verified buttons
|
||||
client.get_info(get_info_callback)
|
||||
|
||||
|
||||
def start_scanning(config, add_entities, client):
|
||||
"""Start a new flic client for scanning & connceting to new buttons."""
|
||||
import pyflic
|
||||
|
||||
scan_wizard = pyflic.ScanWizard()
|
||||
|
||||
def scan_completed_callback(scan_wizard, result, address, name):
|
||||
"""Restart scan wizard to constantly check for new buttons."""
|
||||
if result == pyflic.ScanWizardResult.WizardSuccess:
|
||||
_LOGGER.info("Found new button (%s)", address)
|
||||
elif result != pyflic.ScanWizardResult.WizardFailedTimeout:
|
||||
_LOGGER.warning("Failed to connect to button (%s). Reason: %s",
|
||||
address, result)
|
||||
|
||||
# Restart scan wizard
|
||||
start_scanning(config, add_entities, client)
|
||||
|
||||
scan_wizard.on_completed = scan_completed_callback
|
||||
client.add_scan_wizard(scan_wizard)
|
||||
|
||||
|
||||
def setup_button(hass, config, add_entities, client, address):
|
||||
"""Setup single button device."""
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
ignored_click_types = config.get(CONF_IGNORED_CLICK_TYPES)
|
||||
button = FlicButton(hass, client, address, timeout, ignored_click_types)
|
||||
_LOGGER.info("Connected to button (%s)", address)
|
||||
|
||||
add_entities([button])
|
||||
|
||||
|
||||
class FlicButton(BinarySensorDevice):
|
||||
"""Representation of a flic button."""
|
||||
|
||||
def __init__(self, hass, client, address, timeout, ignored_click_types):
|
||||
"""Initialize the flic button."""
|
||||
import pyflic
|
||||
|
||||
self._hass = hass
|
||||
self._address = address
|
||||
self._timeout = timeout
|
||||
self._is_down = False
|
||||
self._ignored_click_types = ignored_click_types or []
|
||||
self._hass_click_types = {
|
||||
pyflic.ClickType.ButtonClick: CLICK_TYPE_SINGLE,
|
||||
pyflic.ClickType.ButtonSingleClick: CLICK_TYPE_SINGLE,
|
||||
pyflic.ClickType.ButtonDoubleClick: CLICK_TYPE_DOUBLE,
|
||||
pyflic.ClickType.ButtonHold: CLICK_TYPE_HOLD,
|
||||
}
|
||||
|
||||
self._channel = self._create_channel()
|
||||
client.add_connection_channel(self._channel)
|
||||
|
||||
def _create_channel(self):
|
||||
"""Create a new connection channel to the button."""
|
||||
import pyflic
|
||||
|
||||
channel = pyflic.ButtonConnectionChannel(self._address)
|
||||
channel.on_button_up_or_down = self._on_up_down
|
||||
|
||||
# If all types of clicks should be ignored, skip registering callbacks
|
||||
if set(self._ignored_click_types) == set(CLICK_TYPES):
|
||||
return channel
|
||||
|
||||
if CLICK_TYPE_DOUBLE in self._ignored_click_types:
|
||||
# Listen to all but double click type events
|
||||
channel.on_button_click_or_hold = self._on_click
|
||||
elif CLICK_TYPE_HOLD in self._ignored_click_types:
|
||||
# Listen to all but hold click type events
|
||||
channel.on_button_single_or_double_click = self._on_click
|
||||
else:
|
||||
# Listen to all click type events
|
||||
channel.on_button_single_or_double_click_or_hold = self._on_click
|
||||
|
||||
return channel
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return "flic_%s" % self.address.replace(":", "")
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
"""Return the bluetooth address of the device."""
|
||||
return self._address
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._is_down
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
attr = super(FlicButton, self).state_attributes
|
||||
attr["address"] = self.address
|
||||
|
||||
return attr
|
||||
|
||||
def _queued_event_check(self, click_type, time_diff):
|
||||
"""Generate a log message and returns true if timeout exceeded."""
|
||||
time_string = "{:d} {}".format(
|
||||
time_diff, "second" if time_diff == 1 else "seconds")
|
||||
|
||||
if time_diff > self._timeout:
|
||||
_LOGGER.warning(
|
||||
"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
|
||||
|
||||
def _on_up_down(self, channel, click_type, was_queued, time_diff):
|
||||
"""Update device state, if event was not queued."""
|
||||
import pyflic
|
||||
|
||||
if was_queued and self._queued_event_check(click_type, time_diff):
|
||||
return
|
||||
|
||||
self._is_down = click_type == pyflic.ClickType.ButtonDown
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _on_click(self, channel, click_type, was_queued, time_diff):
|
||||
"""Fire click event, if event was not queued."""
|
||||
# Return if click event was queued beyond allowed timeout
|
||||
if was_queued and self._queued_event_check(click_type, time_diff):
|
||||
return
|
||||
|
||||
# Return if click event is in ignored click types
|
||||
hass_click_type = self._hass_click_types[click_type]
|
||||
if hass_click_type in self._ignored_click_types:
|
||||
return
|
||||
|
||||
self._hass.bus.fire(EVENT_NAME, {
|
||||
EVENT_DATA_NAME: self.name,
|
||||
EVENT_DATA_ADDRESS: self.address,
|
||||
EVENT_DATA_QUEUED_TIME: time_diff,
|
||||
EVENT_DATA_TYPE: hass_click_type
|
||||
})
|
||||
|
||||
def _connection_status_changed(self, channel,
|
||||
connection_status, disconnect_reason):
|
||||
"""Remove device, if button disconnects."""
|
||||
import pyflic
|
||||
|
||||
if connection_status == pyflic.ConnectionStatus.Disconnected:
|
||||
_LOGGER.info("Button (%s) disconnected. Reason: %s",
|
||||
self.address, disconnect_reason)
|
||||
self.remove()
|
||||
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
Support for Hikvision event stream events represented as binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.hikvision/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
|
||||
REQUIREMENTS = ['pyhik==0.0.7', 'pydispatcher==2.0.5']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IGNORED = 'ignored'
|
||||
CONF_DELAY = 'delay'
|
||||
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_IGNORED = False
|
||||
DEFAULT_DELAY = 0
|
||||
|
||||
ATTR_DELAY = 'delay'
|
||||
|
||||
SENSOR_CLASS_MAP = {
|
||||
'Motion': 'motion',
|
||||
'Line Crossing': 'motion',
|
||||
'IO Trigger': None,
|
||||
'Field Detection': 'motion',
|
||||
'Video Loss': None,
|
||||
'Tamper Detection': 'motion',
|
||||
'Shelter Alarm': None,
|
||||
'Disk Full': None,
|
||||
'Disk Error': None,
|
||||
'Net Interface Broken': 'connectivity',
|
||||
'IP Conflict': 'connectivity',
|
||||
'Illegal Access': None,
|
||||
'Video Mismatch': None,
|
||||
'Bad Video': None,
|
||||
'PIR Alarm': 'motion',
|
||||
'Face Detection': 'motion',
|
||||
}
|
||||
|
||||
CUSTOMIZE_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_IGNORED, default=DEFAULT_IGNORED): cv.boolean,
|
||||
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_int
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=None): cv.string,
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_SSL, default=False): cv.boolean,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_CUSTOMIZE, default={}):
|
||||
vol.Schema({cv.string: CUSTOMIZE_SCHEMA}),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Setup Hikvision binary sensor devices."""
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
customize = config.get(CONF_CUSTOMIZE)
|
||||
|
||||
if config.get(CONF_SSL):
|
||||
protocol = "https"
|
||||
else:
|
||||
protocol = "http"
|
||||
|
||||
url = '{}://{}'.format(protocol, host)
|
||||
|
||||
data = HikvisionData(hass, url, port, name, username, password)
|
||||
|
||||
if data.sensors is None:
|
||||
_LOGGER.error('Hikvision event stream has no data, unable to setup.')
|
||||
return False
|
||||
|
||||
entities = []
|
||||
|
||||
for sensor in data.sensors:
|
||||
# Build sensor name, then parse customize config.
|
||||
sensor_name = sensor.replace(' ', '_')
|
||||
|
||||
custom = customize.get(sensor_name.lower(), {})
|
||||
ignore = custom.get(CONF_IGNORED)
|
||||
delay = custom.get(CONF_DELAY)
|
||||
|
||||
_LOGGER.debug('Entity: %s - %s, Options - Ignore: %s, Delay: %s',
|
||||
data.name, sensor_name, ignore, delay)
|
||||
if not ignore:
|
||||
entities.append(HikvisionBinarySensor(hass, sensor, data, delay))
|
||||
|
||||
add_entities(entities)
|
||||
|
||||
|
||||
class HikvisionData(object):
|
||||
"""Hikvision camera event stream object."""
|
||||
|
||||
def __init__(self, hass, url, port, name, username, password):
|
||||
"""Initialize the data oject."""
|
||||
from pyhik.hikvision import HikCamera
|
||||
self._url = url
|
||||
self._port = port
|
||||
self._name = name
|
||||
self._username = username
|
||||
self._password = password
|
||||
|
||||
# Establish camera
|
||||
self._cam = HikCamera(self._url, self._port,
|
||||
self._username, self._password)
|
||||
|
||||
if self._name is None:
|
||||
self._name = self._cam.get_name
|
||||
|
||||
# Start event stream
|
||||
self._cam.start_stream()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop_hik)
|
||||
|
||||
def stop_hik(self, event):
|
||||
"""Shutdown Hikvision subscriptions and subscription thread on exit."""
|
||||
self._cam.disconnect()
|
||||
|
||||
@property
|
||||
def sensors(self):
|
||||
"""Return list of available sensors and their states."""
|
||||
return self._cam.current_event_states
|
||||
|
||||
@property
|
||||
def cam_id(self):
|
||||
"""Return camera id."""
|
||||
return self._cam.get_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return camera name."""
|
||||
return self._name
|
||||
|
||||
|
||||
class HikvisionBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Hikvision binary sensor."""
|
||||
|
||||
def __init__(self, hass, sensor, cam, delay):
|
||||
"""Initialize the binary_sensor."""
|
||||
from pydispatch import dispatcher
|
||||
|
||||
self._hass = hass
|
||||
self._cam = cam
|
||||
self._name = self._cam.name + ' ' + sensor
|
||||
self._id = self._cam.cam_id + '.' + sensor
|
||||
self._sensor = sensor
|
||||
|
||||
if delay is None:
|
||||
self._delay = 0
|
||||
else:
|
||||
self._delay = delay
|
||||
|
||||
self._timer = None
|
||||
|
||||
# Form signal for dispatcher
|
||||
signal = 'ValueChanged.{}'.format(self._cam.cam_id)
|
||||
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=signal,
|
||||
sender=self._sensor)
|
||||
|
||||
def _sensor_state(self):
|
||||
"""Extract sensor state."""
|
||||
return self._cam.sensors[self._sensor][0]
|
||||
|
||||
def _sensor_last_update(self):
|
||||
"""Extract sensor last update time."""
|
||||
return self._cam.sensors[self._sensor][3]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the Hikvision sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique ID."""
|
||||
return '{}.{}'.format(self.__class__, self._id)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._sensor_state()
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
try:
|
||||
return SENSOR_CLASS_MAP[self._sensor]
|
||||
except KeyError:
|
||||
# Sensor must be unknown to us, add as generic
|
||||
return None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attr = {}
|
||||
attr[ATTR_LAST_TRIP_TIME] = self._sensor_last_update()
|
||||
|
||||
if self._delay != 0:
|
||||
attr[ATTR_DELAY] = self._delay
|
||||
|
||||
return attr
|
||||
|
||||
def _update_callback(self, signal, sender):
|
||||
"""Update the sensor's state, if needed."""
|
||||
_LOGGER.debug('Dispatcher callback, signal: %s, sender: %s',
|
||||
signal, sender)
|
||||
|
||||
if sender is not self._sensor:
|
||||
return
|
||||
|
||||
if self._delay > 0 and not self.is_on:
|
||||
# Set timer to wait until updating the state
|
||||
def _delay_update(now):
|
||||
"""Timer callback for sensor update."""
|
||||
_LOGGER.debug('%s Called delayed (%ssec) update.',
|
||||
self._name, self._delay)
|
||||
self.schedule_update_ha_state()
|
||||
self._timer = None
|
||||
|
||||
if self._timer is not None:
|
||||
self._timer()
|
||||
self._timer = None
|
||||
|
||||
self._timer = track_point_in_utc_time(
|
||||
self._hass, _delay_update,
|
||||
utcnow() + timedelta(seconds=self._delay))
|
||||
|
||||
elif self._delay > 0 and self.is_on:
|
||||
# For delayed sensors kill any callbacks on true events and update
|
||||
if self._timer is not None:
|
||||
self._timer()
|
||||
self._timer = None
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
else:
|
||||
self.schedule_update_ha_state()
|
||||
@@ -47,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
devices = {}
|
||||
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
||||
map_sv_types, devices, add_devices, MySensorsBinarySensor))
|
||||
map_sv_types, devices, MySensorsBinarySensor, add_devices))
|
||||
|
||||
|
||||
class MySensorsBinarySensor(
|
||||
|
||||
@@ -13,8 +13,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.sensor.nest import NestSensor
|
||||
from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS)
|
||||
from homeassistant.components.nest import (
|
||||
DATA_NEST, is_thermostat, is_camera)
|
||||
from homeassistant.components.nest import DATA_NEST
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
@@ -76,9 +75,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.error(wstr)
|
||||
|
||||
sensors = []
|
||||
device_chain = chain(nest.devices(),
|
||||
nest.protect_devices(),
|
||||
nest.camera_devices())
|
||||
device_chain = chain(nest.thermostats(),
|
||||
nest.smoke_co_alarms(),
|
||||
nest.cameras())
|
||||
for structure, device in device_chain:
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conf
|
||||
@@ -86,9 +85,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conf
|
||||
if variable in CLIMATE_BINARY_TYPES
|
||||
and is_thermostat(device)]
|
||||
and device.is_thermostat]
|
||||
|
||||
if is_camera(device):
|
||||
if device.is_camera:
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conf
|
||||
if variable in CAMERA_BINARY_TYPES]
|
||||
@@ -118,13 +117,14 @@ class NestActivityZoneSensor(NestBinarySensor):
|
||||
|
||||
def __init__(self, structure, device, zone):
|
||||
"""Initialize the sensor."""
|
||||
super(NestActivityZoneSensor, self).__init__(structure, device, None)
|
||||
super(NestActivityZoneSensor, self).__init__(structure, device, "")
|
||||
self.zone = zone
|
||||
self._name = "{} {} activity".format(self._name, self.zone.name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the nest, if any."""
|
||||
return "{} {} activity".format(self._name, self.zone.name)
|
||||
return self._name
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
|
||||
@@ -23,9 +23,11 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# These are the available sensors mapped to binary_sensor class
|
||||
SENSOR_TYPES = {
|
||||
"Someone known": "motion",
|
||||
"Someone unknown": "motion",
|
||||
"Motion": "motion",
|
||||
"Someone known": 'occupancy',
|
||||
"Someone unknown": 'motion',
|
||||
"Motion": 'motion',
|
||||
"Tag Vibration": 'vibration',
|
||||
"Tag Open": 'opening',
|
||||
}
|
||||
|
||||
CONF_HOME = 'home'
|
||||
@@ -48,6 +50,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
home = config.get(CONF_HOME, None)
|
||||
timeout = config.get(CONF_TIMEOUT, 15)
|
||||
|
||||
module_name = None
|
||||
|
||||
import lnetatmo
|
||||
try:
|
||||
data = WelcomeData(netatmo.NETATMO_AUTH, home)
|
||||
@@ -64,23 +68,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
for variable in sensors:
|
||||
add_devices([WelcomeBinarySensor(data, camera_name, home, timeout,
|
||||
variable)])
|
||||
if variable in ('Tag Vibration', 'Tag Open'):
|
||||
continue
|
||||
add_devices([WelcomeBinarySensor(data, camera_name, module_name,
|
||||
home, timeout, variable)])
|
||||
|
||||
for module_name in data.get_module_names(camera_name):
|
||||
for variable in sensors:
|
||||
if variable in ('Tag Vibration', 'Tag Open'):
|
||||
add_devices([WelcomeBinarySensor(data, camera_name,
|
||||
module_name, home,
|
||||
timeout, variable)])
|
||||
|
||||
|
||||
class WelcomeBinarySensor(BinarySensorDevice):
|
||||
"""Represent a single binary sensor in a Netatmo Welcome device."""
|
||||
|
||||
def __init__(self, data, camera_name, home, timeout, sensor):
|
||||
def __init__(self, data, camera_name, module_name, home, timeout, sensor):
|
||||
"""Setup for access to the Netatmo camera events."""
|
||||
self._data = data
|
||||
self._camera_name = camera_name
|
||||
self._module_name = module_name
|
||||
self._home = home
|
||||
self._timeout = timeout
|
||||
if home:
|
||||
self._name = home + ' / ' + camera_name
|
||||
else:
|
||||
self._name = camera_name
|
||||
if module_name:
|
||||
self._name += ' / ' + module_name
|
||||
self._sensor_name = sensor
|
||||
self._name += ' ' + sensor
|
||||
camera_id = data.welcomedata.cameraByName(camera=camera_name,
|
||||
@@ -112,7 +128,7 @@ class WelcomeBinarySensor(BinarySensorDevice):
|
||||
def update(self):
|
||||
"""Request an update from the Netatmo API."""
|
||||
self._data.update()
|
||||
self._data.welcomedata.updateEvent(home=self._data.home)
|
||||
self._data.update_event()
|
||||
|
||||
if self._sensor_name == "Someone known":
|
||||
self._state =\
|
||||
@@ -129,5 +145,16 @@ class WelcomeBinarySensor(BinarySensorDevice):
|
||||
self._data.welcomedata.motionDetected(self._home,
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
elif self._sensor_name == "Tag Vibration":
|
||||
self._state =\
|
||||
self._data.welcomedata.moduleMotionDetected(self._home,
|
||||
self._module_name,
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
elif self._sensor_name == "Tag Open":
|
||||
self._state =\
|
||||
self._data.welcomedata.moduleOpened(self._home,
|
||||
self._module_name,
|
||||
self._camera_name)
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.components.binary_sensor import (
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pynx584==0.2']
|
||||
REQUIREMENTS = ['pynx584==0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for sensor in pywink.get_smoke_and_co_detectors():
|
||||
add_devices([WinkBinarySensorDevice(sensor, hass)])
|
||||
|
||||
for hub in pywink.get_hubs():
|
||||
add_devices([WinkHub(hub, hass)])
|
||||
|
||||
|
||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
"""Representation of a Wink binary sensor."""
|
||||
@@ -79,3 +82,24 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return SENSOR_TYPES.get(self.capability)
|
||||
|
||||
|
||||
class WinkHub(WinkDevice, BinarySensorDevice, Entity):
|
||||
"""Representation of a Wink Hub."""
|
||||
|
||||
def __init(self, wink, hass):
|
||||
"""Initialize the hub sensor."""
|
||||
WinkDevice.__init__(self, wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'update needed': self.wink.update_needed(),
|
||||
'firmware version': self.wink.firmware_version()
|
||||
}
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.wink.state()
|
||||
|
||||
@@ -20,6 +20,8 @@ DEPENDENCIES = []
|
||||
PHILIO = 0x013c
|
||||
PHILIO_SLIM_SENSOR = 0x0002
|
||||
PHILIO_SLIM_SENSOR_MOTION = (PHILIO, PHILIO_SLIM_SENSOR, 0)
|
||||
PHILIO_3_IN_1_SENSOR_GEN_4 = 0x000d
|
||||
PHILIO_3_IN_1_SENSOR_GEN_4_MOTION = (PHILIO, PHILIO_3_IN_1_SENSOR_GEN_4, 0)
|
||||
WENZHOU = 0x0118
|
||||
WENZHOU_SLIM_SENSOR_MOTION = (WENZHOU, PHILIO_SLIM_SENSOR, 0)
|
||||
|
||||
@@ -27,6 +29,7 @@ WORKAROUND_NO_OFF_EVENT = 'trigger_no_off_event'
|
||||
|
||||
DEVICE_MAPPINGS = {
|
||||
PHILIO_SLIM_SENSOR_MOTION: WORKAROUND_NO_OFF_EVENT,
|
||||
PHILIO_3_IN_1_SENSOR_GEN_4_MOTION: WORKAROUND_NO_OFF_EVENT,
|
||||
WENZHOU_SLIM_SENSOR_MOTION: WORKAROUND_NO_OFF_EVENT,
|
||||
}
|
||||
|
||||
@@ -96,6 +99,7 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
_LOGGER.debug('Value changed for label %s', self._value.label)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ https://home-assistant.io/components/calendar/
|
||||
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import re
|
||||
|
||||
from homeassistant.components.google import (CONF_OFFSET,
|
||||
@@ -20,6 +22,7 @@ from homeassistant.util import dt
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
DOMAIN = 'calendar'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
@@ -27,7 +30,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for calendars."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, 60, DOMAIN)
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, DOMAIN)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
@@ -144,10 +147,10 @@ class CalendarEventDevice(Entity):
|
||||
def _get_date(date):
|
||||
"""Get the dateTime from date or dateTime as a local."""
|
||||
if 'date' in date:
|
||||
return dt.as_utc(dt.dt.datetime.combine(
|
||||
dt.parse_date(date['date']), dt.dt.time()))
|
||||
return dt.start_of_local_day(dt.dt.datetime.combine(
|
||||
dt.parse_date(date['date']), dt.dt.time.min))
|
||||
else:
|
||||
return 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'])
|
||||
|
||||
@@ -66,7 +66,7 @@ class GoogleCalendarData(object):
|
||||
"""Get the latest data."""
|
||||
service = self.calendar_service.get()
|
||||
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
|
||||
params['timeMin'] = dt.utcnow().isoformat('T')
|
||||
params['timeMin'] = dt.start_of_local_day().isoformat('T')
|
||||
params['calendarId'] = self.calendar_id
|
||||
if self.search:
|
||||
params['q'] = self.search
|
||||
|
||||
@@ -6,18 +6,27 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import hashlib
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_PICTURE
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'camera'
|
||||
DEPENDENCIES = ['http']
|
||||
SCAN_INTERVAL = 30
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
STATE_RECORDING = 'recording'
|
||||
@@ -27,11 +36,45 @@ STATE_IDLE = 'idle'
|
||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_get_image(hass, entity_id, timeout=10):
|
||||
"""Fetch a image from a camera entity."""
|
||||
websession = async_get_clientsession(hass)
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
if state is None:
|
||||
raise HomeAssistantError(
|
||||
"No entity '{0}' for grab a image".format(entity_id))
|
||||
|
||||
url = "{0}{1}".format(
|
||||
hass.config.api.base_url,
|
||||
state.attributes.get(ATTR_ENTITY_PICTURE)
|
||||
)
|
||||
|
||||
response = None
|
||||
try:
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
response = yield from websession.get(url)
|
||||
|
||||
if response.status != 200:
|
||||
raise HomeAssistantError("Error {0} on {1}".format(
|
||||
response.status, url))
|
||||
|
||||
image = yield from response.read()
|
||||
return image
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
raise HomeAssistantError("Can't connect to {0}".format(url))
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.release()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup the camera component."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
hass.http.register_view(CameraImageView(component.entities))
|
||||
hass.http.register_view(CameraMjpegStream(component.entities))
|
||||
@@ -46,11 +89,13 @@ class Camera(Entity):
|
||||
def __init__(self):
|
||||
"""Initialize a camera."""
|
||||
self.is_streaming = False
|
||||
self._access_token = hashlib.sha256(
|
||||
str.encode(str(id(self)))).hexdigest()
|
||||
|
||||
@property
|
||||
def access_token(self):
|
||||
"""Access token for this camera."""
|
||||
return str(id(self))
|
||||
return self._access_token
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -81,15 +126,12 @@ class Camera(Entity):
|
||||
"""Return bytes of camera image."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return bytes of camera image.
|
||||
|
||||
This method must be run in the event loop.
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
image = yield from self.hass.loop.run_in_executor(
|
||||
None, self.camera_image)
|
||||
return image
|
||||
return self.hass.loop.run_in_executor(None, self.camera_image)
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
@@ -131,8 +173,14 @@ class Camera(Entity):
|
||||
yield from response.drain()
|
||||
|
||||
yield from asyncio.sleep(.5)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Close stream by frontend.")
|
||||
response = None
|
||||
|
||||
finally:
|
||||
yield from response.write_eof()
|
||||
if response is not None:
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@@ -201,12 +249,16 @@ class CameraImageView(CameraView):
|
||||
@asyncio.coroutine
|
||||
def handle(self, request, camera):
|
||||
"""Serve camera image."""
|
||||
image = yield from camera.async_camera_image()
|
||||
try:
|
||||
image = yield from camera.async_camera_image()
|
||||
|
||||
if image is None:
|
||||
return web.Response(status=500)
|
||||
if image is None:
|
||||
return web.Response(status=500)
|
||||
|
||||
return web.Response(body=image)
|
||||
return web.Response(body=image)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Close stream by frontend.")
|
||||
|
||||
|
||||
class CameraMjpegStream(CameraView):
|
||||
|
||||
@@ -18,16 +18,26 @@ REQUIREMENTS = ['amcrest==1.0.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_PORT = 80
|
||||
CONF_RESOLUTION = 'resolution'
|
||||
|
||||
DEFAULT_NAME = 'Amcrest Camera'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_RESOLUTION = 'high'
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
|
||||
RESOLUTION_LIST = {
|
||||
'high': 0,
|
||||
'low': 1,
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
@@ -64,13 +74,14 @@ class AmcrestCam(Camera):
|
||||
def __init__(self, device_info, data):
|
||||
"""Initialize an Amcrest camera."""
|
||||
super(AmcrestCam, self).__init__()
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
self._data = data
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)]
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
# Send the request to snap a picture and return raw jpg data
|
||||
response = self._data.camera.snapshot()
|
||||
response = self._data.camera.snapshot(channel=self._resolution)
|
||||
return response.data
|
||||
|
||||
@property
|
||||
|
||||
@@ -84,9 +84,15 @@ class FFmpegCamera(Camera):
|
||||
if not data:
|
||||
break
|
||||
response.write(data)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Close stream by frontend.")
|
||||
response = None
|
||||
|
||||
finally:
|
||||
self.hass.async_add_job(stream.close())
|
||||
yield from response.write_eof()
|
||||
yield from stream.close()
|
||||
if response is not None:
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -124,9 +124,13 @@ class MjpegCamera(Camera):
|
||||
except asyncio.TimeoutError:
|
||||
raise HTTPGatewayTimeout()
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Close stream by frontend.")
|
||||
response = None
|
||||
|
||||
finally:
|
||||
if stream is not None:
|
||||
self.hass.async_add_job(stream.release())
|
||||
stream.close()
|
||||
if response is not None:
|
||||
yield from response.write_eof()
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
camera_devices = hass.data[nest.DATA_NEST].camera_devices()
|
||||
camera_devices = hass.data[nest.DATA_NEST].cameras()
|
||||
cameras = [NestCamera(structure, device)
|
||||
for structure, device in camera_devices]
|
||||
add_devices(cameras, True)
|
||||
@@ -43,7 +43,7 @@ class NestCamera(Camera):
|
||||
self.device = device
|
||||
self._location = None
|
||||
self._name = None
|
||||
self._is_online = None
|
||||
self._online = None
|
||||
self._is_streaming = None
|
||||
self._is_video_history_enabled = False
|
||||
# Default to non-NestAware subscribed, but will be fixed during update
|
||||
@@ -76,7 +76,7 @@ class NestCamera(Camera):
|
||||
"""Cache value from Python-nest."""
|
||||
self._location = self.device.where
|
||||
self._name = self.device.name
|
||||
self._is_online = self.device.is_online
|
||||
self._online = self.device.online
|
||||
self._is_streaming = self.device.is_streaming
|
||||
self._is_video_history_enabled = self.device.is_video_history_enabled
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_FILE_PATH): cv.string,
|
||||
vol.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
|
||||
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_HORIZONTAL_FLIP):
|
||||
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_IMAGE_HEIGHT):
|
||||
vol.Coerce(int),
|
||||
vol.Optional(CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITIY):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
|
||||
|
||||
@@ -276,9 +276,13 @@ class SynologyCamera(Camera):
|
||||
_LOGGER.exception("Error on %s", streaming_url)
|
||||
raise HTTPGatewayTimeout()
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Close stream by frontend.")
|
||||
response = None
|
||||
|
||||
finally:
|
||||
if stream is not None:
|
||||
self.hass.async_add_job(stream.release())
|
||||
stream.close()
|
||||
if response is not None:
|
||||
yield from response.write_eof()
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.const import CONF_PORT
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['uvcclient==0.9.0']
|
||||
REQUIREMENTS = ['uvcclient==0.10.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -4,15 +4,18 @@ Provides functionality to interact with climate devices.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
import functools as ft
|
||||
from numbers import Number
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -23,7 +26,7 @@ from homeassistant.const import (
|
||||
DOMAIN = "climate"
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
SCAN_INTERVAL = 60
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
SERVICE_SET_AWAY_MODE = "set_away_mode"
|
||||
SERVICE_SET_AUX_HEAT = "set_aux_heat"
|
||||
@@ -185,17 +188,38 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup climate devices."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
component.setup(config)
|
||||
yield from component.async_setup(config)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file,
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def away_mode_set_service(service):
|
||||
@asyncio.coroutine
|
||||
def _async_update_climate(target_climate):
|
||||
"""Update climate entity after service stuff."""
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
climate.async_update_ha_state(True))
|
||||
if hasattr(climate, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
else:
|
||||
yield from update_coro
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_away_mode_set_service(service):
|
||||
"""Set away mode on target climate devices."""
|
||||
target_climate = component.extract_from_service(service)
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
away_mode = service.data.get(ATTR_AWAY_MODE)
|
||||
|
||||
@@ -207,21 +231,21 @@ def setup(hass, config):
|
||||
|
||||
for climate in target_climate:
|
||||
if away_mode:
|
||||
climate.turn_away_mode_on()
|
||||
yield from climate.async_turn_away_mode_on()
|
||||
else:
|
||||
climate.turn_away_mode_off()
|
||||
yield from climate.async_turn_away_mode_off()
|
||||
|
||||
if climate.should_poll:
|
||||
climate.update_ha_state(True)
|
||||
yield from _async_update_climate(target_climate)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_AWAY_MODE, away_mode_set_service,
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_AWAY_MODE, async_away_mode_set_service,
|
||||
descriptions.get(SERVICE_SET_AWAY_MODE),
|
||||
schema=SET_AWAY_MODE_SCHEMA)
|
||||
|
||||
def aux_heat_set_service(service):
|
||||
@asyncio.coroutine
|
||||
def async_aux_heat_set_service(service):
|
||||
"""Set auxillary heater on target climate devices."""
|
||||
target_climate = component.extract_from_service(service)
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
aux_heat = service.data.get(ATTR_AUX_HEAT)
|
||||
|
||||
@@ -233,21 +257,21 @@ def setup(hass, config):
|
||||
|
||||
for climate in target_climate:
|
||||
if aux_heat:
|
||||
climate.turn_aux_heat_on()
|
||||
yield from climate.async_turn_aux_heat_on()
|
||||
else:
|
||||
climate.turn_aux_heat_off()
|
||||
yield from climate.async_turn_aux_heat_off()
|
||||
|
||||
if climate.should_poll:
|
||||
climate.update_ha_state(True)
|
||||
yield from _async_update_climate(target_climate)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_AUX_HEAT, aux_heat_set_service,
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_AUX_HEAT, async_aux_heat_set_service,
|
||||
descriptions.get(SERVICE_SET_AUX_HEAT),
|
||||
schema=SET_AUX_HEAT_SCHEMA)
|
||||
|
||||
def temperature_set_service(service):
|
||||
@asyncio.coroutine
|
||||
def async_temperature_set_service(service):
|
||||
"""Set temperature on the target climate devices."""
|
||||
target_climate = component.extract_from_service(service)
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
for climate in target_climate:
|
||||
kwargs = {}
|
||||
@@ -261,18 +285,19 @@ def setup(hass, config):
|
||||
else:
|
||||
kwargs[value] = temp
|
||||
|
||||
climate.set_temperature(**kwargs)
|
||||
if climate.should_poll:
|
||||
climate.update_ha_state(True)
|
||||
yield from climate.async_set_temperature(**kwargs)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_TEMPERATURE, temperature_set_service,
|
||||
yield from _async_update_climate(target_climate)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service,
|
||||
descriptions.get(SERVICE_SET_TEMPERATURE),
|
||||
schema=SET_TEMPERATURE_SCHEMA)
|
||||
|
||||
def humidity_set_service(service):
|
||||
@asyncio.coroutine
|
||||
def async_humidity_set_service(service):
|
||||
"""Set humidity on the target climate devices."""
|
||||
target_climate = component.extract_from_service(service)
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
humidity = service.data.get(ATTR_HUMIDITY)
|
||||
|
||||
@@ -283,19 +308,19 @@ def setup(hass, config):
|
||||
return
|
||||
|
||||
for climate in target_climate:
|
||||
climate.set_humidity(humidity)
|
||||
yield from climate.async_set_humidity(humidity)
|
||||
|
||||
if climate.should_poll:
|
||||
climate.update_ha_state(True)
|
||||
yield from _async_update_climate(target_climate)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_HUMIDITY, humidity_set_service,
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service,
|
||||
descriptions.get(SERVICE_SET_HUMIDITY),
|
||||
schema=SET_HUMIDITY_SCHEMA)
|
||||
|
||||
def fan_mode_set_service(service):
|
||||
@asyncio.coroutine
|
||||
def async_fan_mode_set_service(service):
|
||||
"""Set fan mode on target climate devices."""
|
||||
target_climate = component.extract_from_service(service)
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
fan = service.data.get(ATTR_FAN_MODE)
|
||||
|
||||
@@ -306,19 +331,19 @@ def setup(hass, config):
|
||||
return
|
||||
|
||||
for climate in target_climate:
|
||||
climate.set_fan_mode(fan)
|
||||
yield from climate.async_set_fan_mode(fan)
|
||||
|
||||
if climate.should_poll:
|
||||
climate.update_ha_state(True)
|
||||
yield from _async_update_climate(target_climate)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_FAN_MODE, fan_mode_set_service,
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_FAN_MODE, async_fan_mode_set_service,
|
||||
descriptions.get(SERVICE_SET_FAN_MODE),
|
||||
schema=SET_FAN_MODE_SCHEMA)
|
||||
|
||||
def operation_set_service(service):
|
||||
@asyncio.coroutine
|
||||
def async_operation_set_service(service):
|
||||
"""Set operating mode on the target climate devices."""
|
||||
target_climate = component.extract_from_service(service)
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
operation_mode = service.data.get(ATTR_OPERATION_MODE)
|
||||
|
||||
@@ -329,19 +354,19 @@ def setup(hass, config):
|
||||
return
|
||||
|
||||
for climate in target_climate:
|
||||
climate.set_operation_mode(operation_mode)
|
||||
yield from climate.async_set_operation_mode(operation_mode)
|
||||
|
||||
if climate.should_poll:
|
||||
climate.update_ha_state(True)
|
||||
yield from _async_update_climate(target_climate)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_OPERATION_MODE, operation_set_service,
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_OPERATION_MODE, async_operation_set_service,
|
||||
descriptions.get(SERVICE_SET_OPERATION_MODE),
|
||||
schema=SET_OPERATION_MODE_SCHEMA)
|
||||
|
||||
def swing_set_service(service):
|
||||
@asyncio.coroutine
|
||||
def async_swing_set_service(service):
|
||||
"""Set swing mode on the target climate devices."""
|
||||
target_climate = component.extract_from_service(service)
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
swing_mode = service.data.get(ATTR_SWING_MODE)
|
||||
|
||||
@@ -352,15 +377,15 @@ def setup(hass, config):
|
||||
return
|
||||
|
||||
for climate in target_climate:
|
||||
climate.set_swing_mode(swing_mode)
|
||||
yield from climate.async_set_swing_mode(swing_mode)
|
||||
|
||||
if climate.should_poll:
|
||||
climate.update_ha_state(True)
|
||||
yield from _async_update_climate(target_climate)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_SWING_MODE, swing_set_service,
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service,
|
||||
descriptions.get(SERVICE_SET_SWING_MODE),
|
||||
schema=SET_SWING_MODE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -521,38 +546,110 @@ class ClimateDevice(Entity):
|
||||
"""Set new target temperature."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.set_temperature, **kwargs))
|
||||
|
||||
def set_humidity(self, humidity):
|
||||
"""Set new target humidity."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_set_humidity(self, humidity):
|
||||
"""Set new target humidity.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.set_humidity, humidity)
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_set_fan_mode(self, fan):
|
||||
"""Set new target fan mode.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.set_fan_mode, fan)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.set_operation_mode, operation_mode)
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing operation."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing operation.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.set_swing_mode, swing_mode)
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_away_mode_on(self):
|
||||
"""Turn away mode on.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.turn_away_mode_on)
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_away_mode_off(self):
|
||||
"""Turn away mode off.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.turn_away_mode_off)
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.turn_aux_heat_on)
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.turn_aux_heat_off)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
|
||||
@@ -22,16 +22,25 @@ _CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_FAN_MIN_ON_TIME = 'fan_min_on_time'
|
||||
ATTR_RESUME_ALL = 'resume_all'
|
||||
|
||||
DEFAULT_RESUME_ALL = False
|
||||
|
||||
DEPENDENCIES = ['ecobee']
|
||||
|
||||
SERVICE_SET_FAN_MIN_ON_TIME = 'ecobee_set_fan_min_on_time'
|
||||
SERVICE_RESUME_PROGRAM = 'ecobee_resume_program'
|
||||
|
||||
SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int),
|
||||
})
|
||||
|
||||
RESUME_PROGRAM_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_RESUME_ALL, default=DEFAULT_RESUME_ALL): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Ecobee Thermostat Platform."""
|
||||
@@ -48,21 +57,36 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
def fan_min_on_time_set_service(service):
|
||||
"""Set the minimum fan on time on the target thermostats."""
|
||||
entity_id = service.data.get('entity_id')
|
||||
entity_id = service.data.get(ATTR_ENTITY_ID)
|
||||
fan_min_on_time = service.data[ATTR_FAN_MIN_ON_TIME]
|
||||
|
||||
if entity_id:
|
||||
target_thermostats = [device for device in devices
|
||||
if device.entity_id == entity_id]
|
||||
if device.entity_id in entity_id]
|
||||
else:
|
||||
target_thermostats = devices
|
||||
|
||||
fan_min_on_time = service.data[ATTR_FAN_MIN_ON_TIME]
|
||||
|
||||
for thermostat in target_thermostats:
|
||||
thermostat.set_fan_min_on_time(str(fan_min_on_time))
|
||||
|
||||
thermostat.update_ha_state(True)
|
||||
|
||||
def resume_program_set_service(service):
|
||||
"""Resume the program on the target thermostats."""
|
||||
entity_id = service.data.get(ATTR_ENTITY_ID)
|
||||
resume_all = service.data.get(ATTR_RESUME_ALL)
|
||||
|
||||
if entity_id:
|
||||
target_thermostats = [device for device in devices
|
||||
if device.entity_id in entity_id]
|
||||
else:
|
||||
target_thermostats = devices
|
||||
|
||||
for thermostat in target_thermostats:
|
||||
thermostat.resume_program(resume_all)
|
||||
|
||||
thermostat.update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@@ -71,6 +95,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
descriptions.get(SERVICE_SET_FAN_MIN_ON_TIME),
|
||||
schema=SET_FAN_MIN_ON_TIME_SCHEMA)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service,
|
||||
descriptions.get(SERVICE_RESUME_PROGRAM),
|
||||
schema=RESUME_PROGRAM_SCHEMA)
|
||||
|
||||
|
||||
class Thermostat(ClimateDevice):
|
||||
"""A thermostat class for Ecobee."""
|
||||
@@ -195,8 +224,9 @@ class Thermostat(ClimateDevice):
|
||||
mode = self.mode
|
||||
events = self.thermostat['events']
|
||||
for event in events:
|
||||
if event['running']:
|
||||
mode = event['holdClimateRef']
|
||||
if event['holdClimateRef'] == 'away' or \
|
||||
event['type'] == 'autoAway':
|
||||
mode = "away"
|
||||
break
|
||||
return 'away' in mode
|
||||
|
||||
@@ -248,6 +278,12 @@ class Thermostat(ClimateDevice):
|
||||
fan_min_on_time)
|
||||
self.update_without_throttle = True
|
||||
|
||||
def resume_program(self, resume_all):
|
||||
"""Resume the thermostat schedule program."""
|
||||
self.data.ecobee.resume_program(self.thermostat_index,
|
||||
str(resume_all).lower())
|
||||
self.update_without_throttle = True
|
||||
|
||||
# Home and Sleep mode aren't used in UI yet:
|
||||
|
||||
# def turn_home_mode_on(self):
|
||||
|
||||
@@ -8,18 +8,27 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, PLATFORM_SCHEMA, PRECISION_HALVES,
|
||||
STATE_UNKNOWN, STATE_AUTO, STATE_ON, STATE_OFF,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_MAC, TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE)
|
||||
from homeassistant.util.temperature import convert
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['bluepy_devices==0.2.0']
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_MODE = 'mode'
|
||||
ATTR_MODE_READABLE = 'mode_readable'
|
||||
STATE_BOOST = "boost"
|
||||
STATE_AWAY = "away"
|
||||
STATE_MANUAL = "manual"
|
||||
|
||||
ATTR_STATE_WINDOW_OPEN = "window_open"
|
||||
ATTR_STATE_VALVE = "valve"
|
||||
ATTR_STATE_LOCKED = "is_locked"
|
||||
ATTR_STATE_LOW_BAT = "low_battery"
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_MAC): cv.string,
|
||||
@@ -48,10 +57,23 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
|
||||
def __init__(self, _mac, _name):
|
||||
"""Initialize the thermostat."""
|
||||
from bluepy_devices.devices import eq3btsmart
|
||||
# we want to avoid name clash with this module..
|
||||
import eq3bt as eq3
|
||||
|
||||
self.modes = {None: STATE_UNKNOWN, # When not yet connected.
|
||||
eq3.Mode.Unknown: STATE_UNKNOWN,
|
||||
eq3.Mode.Auto: STATE_AUTO,
|
||||
# away handled separately, here just for reverse mapping
|
||||
eq3.Mode.Away: STATE_AWAY,
|
||||
eq3.Mode.Closed: STATE_OFF,
|
||||
eq3.Mode.Open: STATE_ON,
|
||||
eq3.Mode.Manual: STATE_MANUAL,
|
||||
eq3.Mode.Boost: STATE_BOOST}
|
||||
|
||||
self.reverse_modes = {v: k for k, v in self.modes.items()}
|
||||
|
||||
self._name = _name
|
||||
self._thermostat = eq3btsmart.EQ3BTSmartThermostat(_mac)
|
||||
self._thermostat = eq3.Thermostat(_mac)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -63,6 +85,11 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
"""Return the unit of measurement that is used."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"""Return eq3bt's precision 0.5."""
|
||||
return PRECISION_HALVES
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Can not report temperature, so return target_temperature."""
|
||||
@@ -81,24 +108,53 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
self._thermostat.target_temperature = temperature
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
return {
|
||||
ATTR_MODE: self._thermostat.mode,
|
||||
ATTR_MODE_READABLE: self._thermostat.mode_readable,
|
||||
}
|
||||
def current_operation(self):
|
||||
"""Current mode."""
|
||||
return self.modes[self._thermostat.mode]
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return [x for x in self.modes.values()]
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
self._thermostat.mode = self.reverse_modes[operation_mode]
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Away mode off turns to AUTO mode."""
|
||||
self.set_operation_mode(STATE_AUTO)
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Set away mode on."""
|
||||
self.set_operation_mode(STATE_AWAY)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return if we are away."""
|
||||
return self.current_operation == STATE_AWAY
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return convert(self._thermostat.min_temp, TEMP_CELSIUS,
|
||||
self.unit_of_measurement)
|
||||
return self._thermostat.min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return convert(self._thermostat.max_temp, TEMP_CELSIUS,
|
||||
self.unit_of_measurement)
|
||||
return self._thermostat.max_temp
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
dev_specific = {
|
||||
ATTR_STATE_LOCKED: self._thermostat.locked,
|
||||
ATTR_STATE_LOW_BAT: self._thermostat.low_battery,
|
||||
ATTR_STATE_VALVE: self._thermostat.valve_state,
|
||||
ATTR_STATE_WINDOW_OPEN: self._thermostat.window_open,
|
||||
}
|
||||
|
||||
return dev_specific
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the thermostat."""
|
||||
|
||||
@@ -198,24 +198,30 @@ class GenericThermostat(ClimateDevice):
|
||||
return
|
||||
|
||||
if self.ac_mode:
|
||||
too_hot = self._cur_temp - self._target_temp > self._tolerance
|
||||
is_cooling = self._is_device_active
|
||||
if too_hot and not is_cooling:
|
||||
_LOGGER.info('Turning on AC %s', self.heater_entity_id)
|
||||
switch.turn_on(self.hass, self.heater_entity_id)
|
||||
elif not too_hot and is_cooling:
|
||||
_LOGGER.info('Turning off AC %s', self.heater_entity_id)
|
||||
switch.turn_off(self.hass, self.heater_entity_id)
|
||||
if is_cooling:
|
||||
too_cold = self._target_temp - self._cur_temp > self._tolerance
|
||||
if too_cold:
|
||||
_LOGGER.info('Turning off AC %s', self.heater_entity_id)
|
||||
switch.turn_off(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
too_hot = self._cur_temp - self._target_temp > self._tolerance
|
||||
if too_hot:
|
||||
_LOGGER.info('Turning on AC %s', self.heater_entity_id)
|
||||
switch.turn_on(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
too_cold = self._target_temp - self._cur_temp > self._tolerance
|
||||
is_heating = self._is_device_active
|
||||
|
||||
if too_cold and not is_heating:
|
||||
_LOGGER.info('Turning on heater %s', self.heater_entity_id)
|
||||
switch.turn_on(self.hass, self.heater_entity_id)
|
||||
elif not too_cold and is_heating:
|
||||
_LOGGER.info('Turning off heater %s', self.heater_entity_id)
|
||||
switch.turn_off(self.hass, self.heater_entity_id)
|
||||
if is_heating:
|
||||
too_hot = self._cur_temp - self._target_temp > self._tolerance
|
||||
if too_hot:
|
||||
_LOGGER.info('Turning off heater %s',
|
||||
self.heater_entity_id)
|
||||
switch.turn_off(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
too_cold = self._target_temp - self._cur_temp > self._tolerance
|
||||
if too_cold:
|
||||
_LOGGER.info('Turning on heater %s', self.heater_entity_id)
|
||||
switch.turn_on(self.hass, self.heater_entity_id)
|
||||
|
||||
@property
|
||||
def _is_device_active(self):
|
||||
|
||||
@@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
}
|
||||
devices = {}
|
||||
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
||||
map_sv_types, devices, add_devices, MySensorsHVAC))
|
||||
map_sv_types, devices, MySensorsHVAC, add_devices))
|
||||
|
||||
|
||||
class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
||||
|
||||
@@ -40,7 +40,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
add_devices(
|
||||
[NestThermostat(structure, device, temp_unit)
|
||||
for structure, device in hass.data[DATA_NEST].devices()],
|
||||
for structure, device in hass.data[DATA_NEST].thermostats()],
|
||||
True
|
||||
)
|
||||
|
||||
@@ -86,6 +86,8 @@ class NestThermostat(ClimateDevice):
|
||||
self._eco_temperature = None
|
||||
self._is_locked = None
|
||||
self._locked_temperature = None
|
||||
self._min_temperature = None
|
||||
self._max_temperature = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -153,8 +155,8 @@ class NestThermostat(ClimateDevice):
|
||||
"""Set new target temperature."""
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if target_temp_low is not None and target_temp_high is not None:
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
if target_temp_low is not None and target_temp_high is not None:
|
||||
temp = (target_temp_low, target_temp_high)
|
||||
else:
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
@@ -204,18 +206,12 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Identify min_temp in Nest API or defaults if not available."""
|
||||
if self._is_locked:
|
||||
return self._locked_temperature[0]
|
||||
else:
|
||||
return None
|
||||
return self._min_temperature
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Identify max_temp in Nest API or defaults if not available."""
|
||||
if self._is_locked:
|
||||
return self._locked_temperature[1]
|
||||
else:
|
||||
return None
|
||||
return self._max_temperature
|
||||
|
||||
def update(self):
|
||||
"""Cache value from Python-nest."""
|
||||
@@ -229,6 +225,8 @@ class NestThermostat(ClimateDevice):
|
||||
self._away = self.structure.away == 'away'
|
||||
self._eco_temperature = self.device.eco_temperature
|
||||
self._locked_temperature = self.device.locked_temperature
|
||||
self._min_temperature = self.device.min_temperature
|
||||
self._max_temperature = self.device.max_temperature
|
||||
self._is_locked = self.device.is_locked
|
||||
if self.device.temperature_scale == 'C':
|
||||
self._temperature_scale = TEMP_CELSIUS
|
||||
|
||||
@@ -23,10 +23,19 @@ ATTR_FAN = 'fan'
|
||||
ATTR_MODE = 'mode'
|
||||
|
||||
CONF_HOLD_TEMP = 'hold_temp'
|
||||
CONF_AWAY_TEMPERATURE_HEAT = 'away_temperature_heat'
|
||||
CONF_AWAY_TEMPERATURE_COOL = 'away_temperature_cool'
|
||||
|
||||
DEFAULT_AWAY_TEMPERATURE_HEAT = 60
|
||||
DEFAULT_AWAY_TEMPERATURE_COOL = 85
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean,
|
||||
vol.Optional(CONF_AWAY_TEMPERATURE_HEAT,
|
||||
default=DEFAULT_AWAY_TEMPERATURE_HEAT): vol.Coerce(float),
|
||||
vol.Optional(CONF_AWAY_TEMPERATURE_COOL,
|
||||
default=DEFAULT_AWAY_TEMPERATURE_COOL): vol.Coerce(float),
|
||||
})
|
||||
|
||||
|
||||
@@ -45,12 +54,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return False
|
||||
|
||||
hold_temp = config.get(CONF_HOLD_TEMP)
|
||||
away_temps = [
|
||||
config.get(CONF_AWAY_TEMPERATURE_HEAT),
|
||||
config.get(CONF_AWAY_TEMPERATURE_COOL)
|
||||
]
|
||||
tstats = []
|
||||
|
||||
for host in hosts:
|
||||
try:
|
||||
tstat = radiotherm.get_thermostat(host)
|
||||
tstats.append(RadioThermostat(tstat, hold_temp))
|
||||
tstats.append(RadioThermostat(tstat, hold_temp, away_temps))
|
||||
except OSError:
|
||||
_LOGGER.exception("Unable to connect to Radio Thermostat: %s",
|
||||
host)
|
||||
@@ -61,7 +74,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class RadioThermostat(ClimateDevice):
|
||||
"""Representation of a Radio Thermostat."""
|
||||
|
||||
def __init__(self, device, hold_temp):
|
||||
def __init__(self, device, hold_temp, away_temps):
|
||||
"""Initialize the thermostat."""
|
||||
self.device = device
|
||||
self.set_time()
|
||||
@@ -71,7 +84,10 @@ class RadioThermostat(ClimateDevice):
|
||||
self._name = None
|
||||
self._fmode = None
|
||||
self._tmode = None
|
||||
self.hold_temp = hold_temp
|
||||
self._hold_temp = hold_temp
|
||||
self._away = False
|
||||
self._away_temps = away_temps
|
||||
self._prev_temp = None
|
||||
self.update()
|
||||
self._operation_list = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF]
|
||||
|
||||
@@ -113,6 +129,11 @@ class RadioThermostat(ClimateDevice):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._away
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the thermostat."""
|
||||
self._current_temperature = self.device.temp['raw']
|
||||
@@ -138,7 +159,7 @@ 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
|
||||
if self.hold_temp:
|
||||
if self._hold_temp or self._away:
|
||||
self.device.hold = 1
|
||||
else:
|
||||
self.device.hold = 0
|
||||
@@ -162,3 +183,23 @@ class RadioThermostat(ClimateDevice):
|
||||
self.device.t_cool = round(self._target_temperature * 2.0) / 2.0
|
||||
elif operation_mode == STATE_HEAT:
|
||||
self.device.t_heat = round(self._target_temperature * 2.0) / 2.0
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on.
|
||||
|
||||
The RTCOA app simulates away mode by using a hold.
|
||||
"""
|
||||
away_temp = None
|
||||
if not self._away:
|
||||
self._prev_temp = self._target_temperature
|
||||
if self._current_operation == STATE_HEAT:
|
||||
away_temp = self._away_temps[0]
|
||||
elif self._current_operation == STATE_COOL:
|
||||
away_temp = self._away_temps[1]
|
||||
self._away = True
|
||||
self.set_temperature(temperature=away_temp)
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self._away = False
|
||||
self.set_temperature(temperature=self._prev_temp)
|
||||
|
||||
@@ -76,7 +76,7 @@ set_operation_mode:
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
example: 'climet.nest'
|
||||
example: 'climate.nest'
|
||||
|
||||
operation_mode:
|
||||
description: New value of operation mode
|
||||
@@ -94,3 +94,27 @@ set_swing_mode:
|
||||
swing_mode:
|
||||
description: New value of swing mode
|
||||
example: 1
|
||||
|
||||
ecobee_set_fan_min_on_time:
|
||||
description: Set the minimum fan on time
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
example: 'climate.kitchen'
|
||||
|
||||
fan_min_on_time:
|
||||
description: New value of fan min on time
|
||||
example: 5
|
||||
|
||||
ecobee_resume_program:
|
||||
description: Resume the programmed schedule
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
example: 'climate.kitchen'
|
||||
|
||||
resume_all:
|
||||
description: Resume all events and return to the scheduled program. This default to false which removes only the top event.
|
||||
example: true
|
||||
|
||||
@@ -89,9 +89,9 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
_LOGGER.debug('Value changed for label %s', self._value.label)
|
||||
self.update_properties()
|
||||
self.schedule_update_ha_state()
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data change for the registered node/value pair."""
|
||||
|
||||
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover/
|
||||
"""
|
||||
import os
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -23,7 +24,7 @@ from homeassistant.const import (
|
||||
|
||||
|
||||
DOMAIN = 'cover'
|
||||
SCAN_INTERVAL = 15
|
||||
SCAN_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
GROUP_NAME_ALL_COVERS = 'all covers'
|
||||
ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format('all_covers')
|
||||
@@ -135,12 +136,19 @@ def setup(hass, config):
|
||||
params = service.data.copy()
|
||||
params.pop(ATTR_ENTITY_ID, None)
|
||||
|
||||
if method:
|
||||
for cover in component.extract_from_service(service):
|
||||
getattr(cover, method['method'])(**params)
|
||||
if not method:
|
||||
return
|
||||
|
||||
if cover.should_poll:
|
||||
cover.update_ha_state(True)
|
||||
covers = component.extract_from_service(service)
|
||||
|
||||
for cover in covers:
|
||||
getattr(cover, method['method'])(**params)
|
||||
|
||||
for cover in covers:
|
||||
if not cover.should_poll:
|
||||
continue
|
||||
|
||||
cover.update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@@ -35,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
})
|
||||
devices = {}
|
||||
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
||||
map_sv_types, devices, add_devices, MySensorsCover))
|
||||
map_sv_types, devices, MySensorsCover, add_devices))
|
||||
|
||||
|
||||
class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice):
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
Support for Tellstick covers using Tellstick Net.
|
||||
|
||||
This platform uses the Telldus Live online service.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.tellduslive/
|
||||
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.tellduslive import TelldusLiveEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup covers."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
add_devices(TelldusLiveCover(hass, cover) for cover in discovery_info)
|
||||
|
||||
|
||||
class TelldusLiveCover(TelldusLiveEntity, CoverDevice):
|
||||
"""Representation of a cover."""
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return the current position of the cover."""
|
||||
return self.device.is_down
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self.device.down()
|
||||
self.changed()
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self.device.up()
|
||||
self.changed()
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self.device.stop()
|
||||
self.changed()
|
||||
@@ -78,9 +78,9 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
_LOGGER.debug('Value changed for label %s', self._value.label)
|
||||
self.update_properties()
|
||||
self.update_ha_state()
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data change for the registered node/value pair."""
|
||||
@@ -170,9 +170,9 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id:
|
||||
_LOGGER.debug('Value changed for label %s', self._value.label)
|
||||
self._state = value.data
|
||||
self.update_ha_state()
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
|
||||
@@ -23,12 +23,14 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||
'cover',
|
||||
'device_tracker',
|
||||
'fan',
|
||||
'image_processing',
|
||||
'light',
|
||||
'lock',
|
||||
'media_player',
|
||||
'notify',
|
||||
'sensor',
|
||||
'switch',
|
||||
'tts',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Sequence, Callable
|
||||
from typing import Any, List, Sequence, Callable
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
@@ -24,6 +24,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import config_per_platform, discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util as util
|
||||
@@ -50,10 +51,10 @@ CONF_TRACK_NEW = 'track_new_devices'
|
||||
DEFAULT_TRACK_NEW = True
|
||||
|
||||
CONF_CONSIDER_HOME = 'consider_home'
|
||||
DEFAULT_CONSIDER_HOME = 180
|
||||
DEFAULT_CONSIDER_HOME = timedelta(seconds=180)
|
||||
|
||||
CONF_SCAN_INTERVAL = 'interval_seconds'
|
||||
DEFAULT_SCAN_INTERVAL = 12
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=12)
|
||||
|
||||
CONF_AWAY_HIDE = 'hide_if_away'
|
||||
DEFAULT_AWAY_HIDE = False
|
||||
@@ -69,12 +70,16 @@ ATTR_LOCATION_NAME = 'location_name'
|
||||
ATTR_GPS = 'gps'
|
||||
ATTR_BATTERY = 'battery'
|
||||
ATTR_ATTRIBUTES = 'attributes'
|
||||
ATTR_SOURCE_TYPE = 'source_type'
|
||||
|
||||
SOURCE_TYPE_GPS = 'gps'
|
||||
SOURCE_TYPE_ROUTER = 'router'
|
||||
|
||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SCAN_INTERVAL): cv.positive_int, # seconds
|
||||
vol.Optional(CONF_SCAN_INTERVAL): cv.time_period,
|
||||
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
|
||||
vol.Optional(CONF_CONSIDER_HOME,
|
||||
default=timedelta(seconds=DEFAULT_CONSIDER_HOME)): vol.All(
|
||||
default=DEFAULT_CONSIDER_HOME): vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
})
|
||||
|
||||
@@ -121,8 +126,7 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
return False
|
||||
else:
|
||||
conf = conf[0] if len(conf) > 0 else {}
|
||||
consider_home = conf.get(CONF_CONSIDER_HOME,
|
||||
timedelta(seconds=DEFAULT_CONSIDER_HOME))
|
||||
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
|
||||
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
||||
|
||||
devices = yield from async_load_config(yaml_path, hass, consider_home)
|
||||
@@ -142,23 +146,34 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
if platform is None:
|
||||
return
|
||||
|
||||
_LOGGER.info("Setting up %s.%s", DOMAIN, p_type)
|
||||
try:
|
||||
if hasattr(platform, 'get_scanner'):
|
||||
scanner = None
|
||||
setup = None
|
||||
if hasattr(platform, 'async_get_scanner'):
|
||||
scanner = yield from platform.async_get_scanner(
|
||||
hass, {DOMAIN: p_config})
|
||||
elif hasattr(platform, 'get_scanner'):
|
||||
scanner = yield from hass.loop.run_in_executor(
|
||||
None, platform.get_scanner, hass, {DOMAIN: p_config})
|
||||
elif hasattr(platform, 'async_setup_scanner'):
|
||||
setup = yield from platform.async_setup_scanner(
|
||||
hass, p_config, tracker.async_see)
|
||||
elif hasattr(platform, 'setup_scanner'):
|
||||
setup = yield from hass.loop.run_in_executor(
|
||||
None, platform.setup_scanner, hass, p_config, tracker.see)
|
||||
else:
|
||||
raise HomeAssistantError("Invalid device_tracker platform.")
|
||||
|
||||
if scanner is None:
|
||||
_LOGGER.error('Error setting up platform %s', p_type)
|
||||
return
|
||||
|
||||
if scanner:
|
||||
yield from async_setup_scanner_platform(
|
||||
hass, p_config, scanner, tracker.async_see)
|
||||
return
|
||||
|
||||
ret = yield from hass.loop.run_in_executor(
|
||||
None, platform.setup_scanner, hass, p_config, tracker.see)
|
||||
if not ret:
|
||||
if not setup:
|
||||
_LOGGER.error('Error setting up platform %s', p_type)
|
||||
return
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception('Error setting up platform %s', p_type)
|
||||
|
||||
@@ -223,17 +238,19 @@ class DeviceTracker(object):
|
||||
|
||||
def see(self, mac: str=None, dev_id: str=None, host_name: str=None,
|
||||
location_name: str=None, gps: GPSType=None, gps_accuracy=None,
|
||||
battery: str=None, attributes: dict=None):
|
||||
battery: str=None, attributes: dict=None,
|
||||
source_type: str=SOURCE_TYPE_GPS):
|
||||
"""Notify the device tracker that you see a device."""
|
||||
self.hass.add_job(
|
||||
self.async_see(mac, dev_id, host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes)
|
||||
gps_accuracy, battery, attributes, source_type)
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_see(self, mac: str=None, dev_id: str=None, host_name: str=None,
|
||||
location_name: str=None, gps: GPSType=None,
|
||||
gps_accuracy=None, battery: str=None, attributes: dict=None):
|
||||
gps_accuracy=None, battery: str=None, attributes: dict=None,
|
||||
source_type: str=SOURCE_TYPE_GPS):
|
||||
"""Notify the device tracker that you see a device.
|
||||
|
||||
This method is a coroutine.
|
||||
@@ -251,7 +268,8 @@ class DeviceTracker(object):
|
||||
|
||||
if device:
|
||||
yield from device.async_seen(host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes)
|
||||
gps_accuracy, battery, attributes,
|
||||
source_type)
|
||||
if device.track:
|
||||
yield from device.async_update_ha_state()
|
||||
return
|
||||
@@ -266,7 +284,8 @@ class DeviceTracker(object):
|
||||
self.mac_to_dev[mac] = device
|
||||
|
||||
yield from device.async_seen(host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes)
|
||||
gps_accuracy, battery, attributes,
|
||||
source_type)
|
||||
|
||||
if device.track:
|
||||
yield from device.async_update_ha_state()
|
||||
@@ -282,7 +301,7 @@ class DeviceTracker(object):
|
||||
list(self.group.tracking) + [device.entity_id])
|
||||
|
||||
# lookup mac vendor string to be stored in config
|
||||
device.set_vendor_for_mac()
|
||||
yield from device.set_vendor_for_mac()
|
||||
|
||||
# update known_devices.yaml
|
||||
self.hass.async_add_job(
|
||||
@@ -371,6 +390,10 @@ class Device(Entity):
|
||||
self.away_hide = hide_if_away
|
||||
self.vendor = vendor
|
||||
|
||||
self.source_type = None
|
||||
|
||||
self._attributes = {}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
@@ -389,7 +412,9 @@ class Device(Entity):
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
attr = {}
|
||||
attr = {
|
||||
ATTR_SOURCE_TYPE: self.source_type
|
||||
}
|
||||
|
||||
if self.gps:
|
||||
attr[ATTR_LATITUDE] = self.gps[0]
|
||||
@@ -399,12 +424,13 @@ class Device(Entity):
|
||||
if self.battery:
|
||||
attr[ATTR_BATTERY] = self.battery
|
||||
|
||||
if self.attributes:
|
||||
for key, value in self.attributes.items():
|
||||
attr[key] = value
|
||||
|
||||
return attr
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device state attributes."""
|
||||
return self._attributes
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
"""If device should be hidden."""
|
||||
@@ -413,20 +439,27 @@ class Device(Entity):
|
||||
@asyncio.coroutine
|
||||
def async_seen(self, host_name: str=None, location_name: str=None,
|
||||
gps: GPSType=None, gps_accuracy=0, battery: str=None,
|
||||
attributes: dict=None):
|
||||
attributes: dict=None, source_type: str=SOURCE_TYPE_GPS):
|
||||
"""Mark the device as seen."""
|
||||
self.source_type = source_type
|
||||
self.last_seen = dt_util.utcnow()
|
||||
self.host_name = host_name
|
||||
self.location_name = location_name
|
||||
self.gps_accuracy = gps_accuracy or 0
|
||||
self.battery = battery
|
||||
self.attributes = attributes
|
||||
|
||||
if battery:
|
||||
self.battery = battery
|
||||
if attributes:
|
||||
self._attributes.update(attributes)
|
||||
|
||||
self.gps = None
|
||||
|
||||
if gps is not None:
|
||||
try:
|
||||
self.gps = float(gps[0]), float(gps[1])
|
||||
self.gps_accuracy = gps_accuracy or 0
|
||||
except (ValueError, TypeError, IndexError):
|
||||
self.gps = None
|
||||
self.gps_accuracy = 0
|
||||
_LOGGER.warning('Could not parse gps value for %s: %s',
|
||||
self.dev_id, gps)
|
||||
|
||||
@@ -451,7 +484,7 @@ class Device(Entity):
|
||||
return
|
||||
elif self.location_name:
|
||||
self._state = self.location_name
|
||||
elif self.gps is not None:
|
||||
elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS:
|
||||
zone_state = zone.async_active_zone(
|
||||
self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
|
||||
if zone_state is None:
|
||||
@@ -460,9 +493,9 @@ class Device(Entity):
|
||||
self._state = STATE_HOME
|
||||
else:
|
||||
self._state = zone_state.name
|
||||
|
||||
elif self.stale():
|
||||
self._state = STATE_NOT_HOME
|
||||
self.gps = None
|
||||
self.last_update_home = False
|
||||
else:
|
||||
self._state = STATE_HOME
|
||||
@@ -480,13 +513,18 @@ class Device(Entity):
|
||||
if not self.mac:
|
||||
return None
|
||||
|
||||
if '_' in self.mac:
|
||||
_, mac = self.mac.split('_', 1)
|
||||
else:
|
||||
mac = self.mac
|
||||
|
||||
# prevent lookup of invalid macs
|
||||
if not len(self.mac.split(':')) == 6:
|
||||
if not len(mac.split(':')) == 6:
|
||||
return 'unknown'
|
||||
|
||||
# we only need the first 3 bytes of the mac for a lookup
|
||||
# this improves somewhat on privacy
|
||||
oui_bytes = self.mac.split(':')[0:3]
|
||||
oui_bytes = mac.split(':')[0:3]
|
||||
# bytes like 00 get truncates to 0, API needs full bytes
|
||||
oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes])
|
||||
url = 'http://api.macvendors.com/' + oui
|
||||
@@ -516,6 +554,34 @@ class Device(Entity):
|
||||
yield from resp.release()
|
||||
|
||||
|
||||
class DeviceScanner(object):
|
||||
"""Device scanner object."""
|
||||
|
||||
hass = None # type: HomeAssistantType
|
||||
|
||||
def scan_devices(self) -> List[str]:
|
||||
"""Scan for devices."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_scan_devices(self) -> Any:
|
||||
"""Scan for devices.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(None, self.scan_devices)
|
||||
|
||||
def get_device_name(self, mac: str) -> str:
|
||||
"""Get device name from mac."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_get_device_name(self, mac: str) -> Any:
|
||||
"""Get device name from mac.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(None, self.get_device_name, mac)
|
||||
|
||||
|
||||
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
"""Load devices from YAML configuration file."""
|
||||
return run_coroutine_threadsafe(
|
||||
@@ -572,26 +638,39 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
This method is a coroutine.
|
||||
"""
|
||||
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
scanner.hass = hass
|
||||
|
||||
# Initial scan of each mac we also tell about host name for config
|
||||
seen = set() # type: Any
|
||||
|
||||
def device_tracker_scan(now: dt_util.dt.datetime):
|
||||
@asyncio.coroutine
|
||||
def async_device_tracker_scan(now: dt_util.dt.datetime):
|
||||
"""Called when interval matches."""
|
||||
found_devices = scanner.scan_devices()
|
||||
found_devices = yield from scanner.async_scan_devices()
|
||||
|
||||
for mac in found_devices:
|
||||
if mac in seen:
|
||||
host_name = None
|
||||
else:
|
||||
host_name = scanner.get_device_name(mac)
|
||||
host_name = yield from scanner.async_get_device_name(mac)
|
||||
seen.add(mac)
|
||||
hass.add_job(async_see_device(mac=mac, host_name=host_name))
|
||||
|
||||
async_track_utc_time_change(
|
||||
hass, device_tracker_scan, second=range(0, 60, interval))
|
||||
kwargs = {
|
||||
'mac': mac,
|
||||
'host_name': host_name,
|
||||
'source_type': SOURCE_TYPE_ROUTER
|
||||
}
|
||||
|
||||
hass.async_add_job(device_tracker_scan, None)
|
||||
zone_home = hass.states.get(zone.ENTITY_ID_HOME)
|
||||
if zone_home:
|
||||
kwargs['gps'] = [zone_home.attributes[ATTR_LATITUDE],
|
||||
zone_home.attributes[ATTR_LONGITUDE]]
|
||||
kwargs['gps_accuracy'] = 0
|
||||
|
||||
hass.async_add_job(async_see_device(**kwargs))
|
||||
|
||||
async_track_time_interval(hass, async_device_tracker_scan, interval)
|
||||
hass.async_add_job(async_device_tracker_scan, None)
|
||||
|
||||
|
||||
def update_config(path: str, dev_id: str, device: Device):
|
||||
|
||||
@@ -14,7 +14,8 @@ import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
@@ -46,7 +47,7 @@ def get_scanner(hass, config):
|
||||
Device = namedtuple("Device", ["mac", "ip", "last_update"])
|
||||
|
||||
|
||||
class ActiontecDeviceScanner(object):
|
||||
class ActiontecDeviceScanner(DeviceScanner):
|
||||
"""This class queries a an actiontec router for connected devices."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
||||
@@ -12,7 +12,8 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
@@ -42,7 +43,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class ArubaDeviceScanner(object):
|
||||
class ArubaDeviceScanner(DeviceScanner):
|
||||
"""This class queries a Aruba Access Point for connected devices."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
||||
@@ -14,7 +14,8 @@ from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -97,7 +98,7 @@ def get_scanner(hass, config):
|
||||
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp nvram')
|
||||
|
||||
|
||||
class AsusWrtDeviceScanner(object):
|
||||
class AsusWrtDeviceScanner(DeviceScanner):
|
||||
"""This class queries a router running ASUSWRT firmware."""
|
||||
|
||||
# Eighth attribute needed for mode (AP mode vs router mode)
|
||||
@@ -286,8 +287,10 @@ class AsusWrtDeviceScanner(object):
|
||||
|
||||
# match mac addresses to IP addresses in ARP table
|
||||
for arp in result.arp:
|
||||
if match.group('mac').lower() in arp.decode('utf-8'):
|
||||
arp_match = _ARP_REGEX.search(arp.decode('utf-8'))
|
||||
if match.group('mac').lower() in \
|
||||
arp.decode('utf-8').lower():
|
||||
arp_match = _ARP_REGEX.search(
|
||||
arp.decode('utf-8').lower())
|
||||
if not arp_match:
|
||||
_LOGGER.warning('Could not parse arp row: %s', arp)
|
||||
continue
|
||||
|
||||
@@ -11,8 +11,8 @@ import requests
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import (PLATFORM_SCHEMA,
|
||||
ATTR_ATTRIBUTES)
|
||||
from homeassistant.components.device_tracker import (
|
||||
PLATFORM_SCHEMA, ATTR_ATTRIBUTES)
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
|
||||
@@ -9,7 +9,7 @@ import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.components.device_tracker import DOMAIN, DeviceScanner
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['pybbox==0.0.5-alpha']
|
||||
@@ -29,7 +29,7 @@ def get_scanner(hass, config):
|
||||
Device = namedtuple('Device', ['mac', 'name', 'ip', 'last_update'])
|
||||
|
||||
|
||||
class BboxDeviceScanner(object):
|
||||
class BboxDeviceScanner(DeviceScanner):
|
||||
"""This class scans for devices connected to the bbox."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
||||
@@ -5,13 +5,8 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.components.device_tracker import (
|
||||
YAML_DEVICES,
|
||||
CONF_TRACK_NEW,
|
||||
CONF_SCAN_INTERVAL,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
PLATFORM_SCHEMA,
|
||||
load_config,
|
||||
DEFAULT_TRACK_NEW
|
||||
YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
|
||||
PLATFORM_SCHEMA, load_config, DEFAULT_TRACK_NEW
|
||||
)
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.dt as dt_util
|
||||
@@ -24,9 +19,11 @@ REQUIREMENTS = ['gattlib==0.20150805']
|
||||
BLE_PREFIX = 'BLE_'
|
||||
MIN_SEEN_NEW = 5
|
||||
CONF_SCAN_DURATION = "scan_duration"
|
||||
CONF_BLUETOOTH_DEVICE = "device_id"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SCAN_DURATION, default=10): cv.positive_int
|
||||
vol.Optional(CONF_SCAN_DURATION, default=10): cv.positive_int,
|
||||
vol.Optional(CONF_BLUETOOTH_DEVICE, default="hci0"): cv.string
|
||||
})
|
||||
|
||||
|
||||
@@ -60,7 +57,7 @@ def setup_scanner(hass, config, see):
|
||||
"""Discover Bluetooth LE devices."""
|
||||
_LOGGER.debug("Discovering Bluetooth LE devices")
|
||||
try:
|
||||
service = DiscoveryService()
|
||||
service = DiscoveryService(ble_dev_id)
|
||||
devices = service.discover(duration)
|
||||
_LOGGER.debug("Bluetooth LE devices discovered = %s", devices)
|
||||
except RuntimeError as error:
|
||||
@@ -70,6 +67,7 @@ def setup_scanner(hass, config, see):
|
||||
|
||||
yaml_path = hass.config.path(YAML_DEVICES)
|
||||
duration = config.get(CONF_SCAN_DURATION)
|
||||
ble_dev_id = config.get(CONF_BLUETOOTH_DEVICE)
|
||||
devs_to_track = []
|
||||
devs_donot_track = []
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
@@ -40,7 +41,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class BTHomeHub5DeviceScanner(object):
|
||||
class BTHomeHub5DeviceScanner(DeviceScanner):
|
||||
"""This class queries a BT Home Hub 5."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
||||
@@ -10,7 +10,8 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \
|
||||
CONF_PORT
|
||||
from homeassistant.util import Throttle
|
||||
@@ -39,7 +40,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class CiscoDeviceScanner(object):
|
||||
class CiscoDeviceScanner(DeviceScanner):
|
||||
"""This class queries a wireless router running Cisco IOS firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
||||
@@ -13,7 +13,8 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
@@ -41,7 +42,7 @@ def get_scanner(hass, config):
|
||||
return None
|
||||
|
||||
|
||||
class DdWrtDeviceScanner(object):
|
||||
class DdWrtDeviceScanner(DeviceScanner):
|
||||
"""This class queries a wireless router running DD-WRT firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
||||
@@ -10,7 +10,8 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
@@ -38,7 +39,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class FritzBoxScanner(object):
|
||||
class FritzBoxScanner(DeviceScanner):
|
||||
"""This class queries a FRITZ!Box router."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
||||
@@ -64,9 +64,22 @@ class GPSLoggerView(HomeAssistantView):
|
||||
if 'battery' in data:
|
||||
battery = float(data['battery'])
|
||||
|
||||
attrs = {}
|
||||
if 'speed' in data:
|
||||
attrs['speed'] = float(data['speed'])
|
||||
if 'direction' in data:
|
||||
attrs['direction'] = float(data['direction'])
|
||||
if 'altitude' in data:
|
||||
attrs['altitude'] = float(data['altitude'])
|
||||
if 'provider' in data:
|
||||
attrs['provider'] = data['provider']
|
||||
if 'activity' in data:
|
||||
attrs['activity'] = data['activity']
|
||||
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, partial(self.see, dev_id=device,
|
||||
gps=gps_location, battery=battery,
|
||||
gps_accuracy=accuracy))
|
||||
gps_accuracy=accuracy,
|
||||
attributes=attrs))
|
||||
|
||||
return 'Setting location for {}'.format(device)
|
||||
|
||||
@@ -12,7 +12,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.components.device_tracker import (
|
||||
PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT)
|
||||
PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT, DeviceScanner)
|
||||
from homeassistant.components.zone import active_zone
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -131,7 +131,7 @@ def setup_scanner(hass, config: dict, see):
|
||||
return True
|
||||
|
||||
|
||||
class Icloud(object):
|
||||
class Icloud(DeviceScanner):
|
||||
"""Represent an icloud account in Home Assistant."""
|
||||
|
||||
def __init__(self, hass, username, password, name, see):
|
||||
|
||||
@@ -8,9 +8,8 @@ import asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
STATE_NOT_HOME,
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE, ATTR_LONGITUDE, STATE_NOT_HOME, HTTP_UNPROCESSABLE_ENTITY)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
# pylint: disable=unused-import
|
||||
from homeassistant.components.device_tracker import ( # NOQA
|
||||
@@ -64,18 +63,18 @@ class LocativeView(HomeAssistantView):
|
||||
return ('Device id not specified.',
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if 'id' not in data:
|
||||
_LOGGER.error('Location id not specified.')
|
||||
return ('Location id not specified.',
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if 'trigger' not in data:
|
||||
_LOGGER.error('Trigger is not specified.')
|
||||
return ('Trigger is not specified.',
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if 'id' not in data and data['trigger'] != 'test':
|
||||
_LOGGER.error('Location id not specified.')
|
||||
return ('Location id not specified.',
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
device = data['device'].replace('-', '')
|
||||
location_name = data['id'].lower()
|
||||
location_name = data.get('id', data['trigger']).lower()
|
||||
direction = data['trigger']
|
||||
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
@@ -37,7 +38,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class LuciDeviceScanner(object):
|
||||
class LuciDeviceScanner(DeviceScanner):
|
||||
"""This class queries a wireless router running OpenWrt firmware.
|
||||
|
||||
Adapted from Tomato scanner.
|
||||
|
||||
@@ -11,7 +11,8 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
|
||||
from homeassistant.util import Throttle
|
||||
@@ -47,7 +48,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class NetgearDeviceScanner(object):
|
||||
class NetgearDeviceScanner(DeviceScanner):
|
||||
"""Queries a Netgear wireless router using the SOAP-API."""
|
||||
|
||||
def __init__(self, host, username, password, port):
|
||||
|
||||
@@ -14,7 +14,8 @@ import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOSTS
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
@@ -25,6 +26,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
CONF_EXCLUDE = 'exclude'
|
||||
# Interval in minutes to exclude devices from a scan while they are home
|
||||
CONF_HOME_INTERVAL = 'home_interval'
|
||||
CONF_OPTIONS = 'scan_options'
|
||||
DEFAULT_OPTIONS = '-F --host-timeout 5s'
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
@@ -33,7 +36,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOSTS): cv.ensure_list,
|
||||
vol.Required(CONF_HOME_INTERVAL, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_EXCLUDE, default=[]):
|
||||
vol.All(cv.ensure_list, vol.Length(min=1))
|
||||
vol.All(cv.ensure_list, vol.Length(min=1)),
|
||||
vol.Optional(CONF_OPTIONS, default=DEFAULT_OPTIONS):
|
||||
cv.string
|
||||
})
|
||||
|
||||
|
||||
@@ -59,7 +64,7 @@ def _arp(ip_address):
|
||||
return None
|
||||
|
||||
|
||||
class NmapDeviceScanner(object):
|
||||
class NmapDeviceScanner(DeviceScanner):
|
||||
"""This class scans for devices using nmap."""
|
||||
|
||||
exclude = []
|
||||
@@ -69,8 +74,9 @@ class NmapDeviceScanner(object):
|
||||
self.last_results = []
|
||||
|
||||
self.hosts = config[CONF_HOSTS]
|
||||
self.exclude = config.get(CONF_EXCLUDE, [])
|
||||
self.exclude = config[CONF_EXCLUDE]
|
||||
minutes = config[CONF_HOME_INTERVAL]
|
||||
self._options = config[CONF_OPTIONS]
|
||||
self.home_interval = timedelta(minutes=minutes)
|
||||
|
||||
self.success_init = self._update_info()
|
||||
@@ -103,7 +109,7 @@ class NmapDeviceScanner(object):
|
||||
from nmap import PortScanner, PortScannerError
|
||||
scanner = PortScanner()
|
||||
|
||||
options = '-F --host-timeout 5s '
|
||||
options = self._options
|
||||
|
||||
if self.home_interval:
|
||||
boundary = dt_util.now() - self.home_interval
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
Tracks devices by sending a ICMP ping.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.ping/
|
||||
|
||||
device_tracker:
|
||||
- platform: ping
|
||||
count: 2
|
||||
hosts:
|
||||
host_one: pc.local
|
||||
host_two: 192.168.2.25
|
||||
"""
|
||||
import logging
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import (
|
||||
PLATFORM_SCHEMA, DEFAULT_SCAN_INTERVAL)
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant import util
|
||||
from homeassistant import const
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = []
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_PING_COUNT = 'count'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(const.CONF_HOSTS): {cv.string: cv.string},
|
||||
vol.Optional(CONF_PING_COUNT, default=1): cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
class Host:
|
||||
"""Host object with ping detection."""
|
||||
|
||||
def __init__(self, ip_address, dev_id, hass, config):
|
||||
"""Initialize the Host pinger."""
|
||||
self.hass = hass
|
||||
self.ip_address = ip_address
|
||||
self.dev_id = dev_id
|
||||
self._count = config[CONF_PING_COUNT]
|
||||
if sys.platform == "win32":
|
||||
self._ping_cmd = ['ping', '-n 1', '-w 1000', self.ip_address]
|
||||
else:
|
||||
self._ping_cmd = ['ping', '-n', '-q', '-c1', '-W1',
|
||||
self.ip_address]
|
||||
|
||||
def ping(self):
|
||||
"""Send ICMP ping and return True if success."""
|
||||
pinger = subprocess.Popen(self._ping_cmd, stdout=subprocess.PIPE)
|
||||
try:
|
||||
pinger.communicate()
|
||||
return pinger.returncode == 0
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
def update(self, see):
|
||||
"""Update device state by sending one or more ping messages."""
|
||||
failed = 0
|
||||
while failed < self._count: # check more times if host in unreachable
|
||||
if self.ping():
|
||||
see(dev_id=self.dev_id)
|
||||
return True
|
||||
failed += 1
|
||||
|
||||
_LOGGER.debug("ping KO on ip=%s failed=%d", self.ip_address, failed)
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
"""Setup the Host objects and return the update function."""
|
||||
hosts = [Host(ip, dev_id, hass, config) for (dev_id, ip) in
|
||||
config[const.CONF_HOSTS].items()]
|
||||
interval = timedelta(seconds=len(hosts) * config[CONF_PING_COUNT]) + \
|
||||
DEFAULT_SCAN_INTERVAL
|
||||
_LOGGER.info("Started ping tracker with interval=%s on hosts: %s",
|
||||
interval, ",".join([host.ip_address for host in hosts]))
|
||||
|
||||
def update(now):
|
||||
"""Update all the hosts on every interval time."""
|
||||
for host in hosts:
|
||||
host.update(see)
|
||||
track_point_in_utc_time(hass, update, now + interval)
|
||||
return True
|
||||
|
||||
return update(util.dt.utcnow())
|
||||
@@ -12,7 +12,8 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
@@ -46,7 +47,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class SnmpScanner(object):
|
||||
class SnmpScanner(DeviceScanner):
|
||||
"""Queries any SNMP capable Access Point for connected devices."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
||||
@@ -12,7 +12,8 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
@@ -35,7 +36,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class SwisscomDeviceScanner(object):
|
||||
class SwisscomDeviceScanner(DeviceScanner):
|
||||
"""This class queries a router running Swisscom Internet-Box firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
||||
@@ -13,7 +13,8 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
@@ -46,7 +47,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class ThomsonDeviceScanner(object):
|
||||
class ThomsonDeviceScanner(DeviceScanner):
|
||||
"""This class queries a router running THOMSON firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
||||
@@ -14,7 +14,8 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
@@ -38,7 +39,7 @@ def get_scanner(hass, config):
|
||||
return TomatoDeviceScanner(config[DOMAIN])
|
||||
|
||||
|
||||
class TomatoDeviceScanner(object):
|
||||
class TomatoDeviceScanner(DeviceScanner):
|
||||
"""This class queries a wireless router running Tomato firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
||||
@@ -15,7 +15,8 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
@@ -42,7 +43,7 @@ def get_scanner(hass, config):
|
||||
return None
|
||||
|
||||
|
||||
class TplinkDeviceScanner(object):
|
||||
class TplinkDeviceScanner(DeviceScanner):
|
||||
"""This class queries a wireless router running TP-Link firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Support for the TrackR platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.trackr/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pytrackr==0.0.5']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config: dict, see):
|
||||
"""Validate the configuration and return a TrackR scanner."""
|
||||
TrackRDeviceScanner(hass, config, see)
|
||||
return True
|
||||
|
||||
|
||||
class TrackRDeviceScanner(object):
|
||||
"""A class representing a TrackR device."""
|
||||
|
||||
def __init__(self, hass, config: dict, see) -> None:
|
||||
"""Initialize the TrackR device scanner."""
|
||||
from pytrackr.api import trackrApiInterface
|
||||
self.hass = hass
|
||||
self.api = trackrApiInterface(config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD))
|
||||
self.see = see
|
||||
self.devices = self.api.get_trackrs()
|
||||
self._update_info()
|
||||
|
||||
track_utc_time_change(self.hass, self._update_info,
|
||||
second=range(0, 60, 30))
|
||||
|
||||
def _update_info(self, now=None) -> None:
|
||||
"""Update the device info."""
|
||||
_LOGGER.debug('Updating devices %s', now)
|
||||
|
||||
# Update self.devices to collect new devices added
|
||||
# to the users account.
|
||||
self.devices = self.api.get_trackrs()
|
||||
|
||||
for trackr in self.devices:
|
||||
trackr.update_state()
|
||||
trackr_id = trackr.tracker_id()
|
||||
trackr_device_id = trackr.id()
|
||||
lost = trackr.lost()
|
||||
dev_id = trackr.name().replace(" ", "_")
|
||||
if dev_id is None:
|
||||
dev_id = trackr_id
|
||||
location = trackr.last_known_location()
|
||||
lat = location['latitude']
|
||||
lon = location['longitude']
|
||||
|
||||
attrs = {
|
||||
'last_updated': trackr.last_updated(),
|
||||
'last_seen': trackr.last_time_seen(),
|
||||
'trackr_id': trackr_id,
|
||||
'id': trackr_device_id,
|
||||
'lost': lost,
|
||||
'battery_level': trackr.battery_level()
|
||||
}
|
||||
|
||||
self.see(
|
||||
dev_id=dev_id, gps=(lat, lon), attributes=attrs
|
||||
)
|
||||
@@ -14,7 +14,8 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
@@ -37,7 +38,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class UbusDeviceScanner(object):
|
||||
class UbusDeviceScanner(DeviceScanner):
|
||||
"""
|
||||
This class queries a wireless router running OpenWrt firmware.
|
||||
|
||||
|
||||
@@ -9,16 +9,21 @@ import urllib
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
|
||||
# Unifi package doesn't list urllib3 as a requirement
|
||||
REQUIREMENTS = ['urllib3', 'unifi==1.2.5']
|
||||
REQUIREMENTS = ['urllib3', 'pyunifi==1.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONF_PORT = 'port'
|
||||
CONF_SITE_ID = 'site_id'
|
||||
|
||||
NOTIFICATION_ID = 'unifi_notification'
|
||||
NOTIFICATION_TITLE = 'Unifi Device Tracker Setup'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST, default='localhost'): cv.string,
|
||||
vol.Optional(CONF_SITE_ID, default='default'): cv.string,
|
||||
@@ -30,7 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Setup Unifi device_tracker."""
|
||||
from unifi.controller import Controller
|
||||
from pyunifi.controller import Controller
|
||||
|
||||
host = config[DOMAIN].get(CONF_HOST)
|
||||
username = config[DOMAIN].get(CONF_USERNAME)
|
||||
@@ -38,16 +43,24 @@ def get_scanner(hass, config):
|
||||
site_id = config[DOMAIN].get(CONF_SITE_ID)
|
||||
port = config[DOMAIN].get(CONF_PORT)
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
try:
|
||||
ctrl = Controller(host, username, password, port, 'v4', site_id)
|
||||
except urllib.error.HTTPError as ex:
|
||||
_LOGGER.error('Failed to connect to unifi: %s', ex)
|
||||
_LOGGER.error('Failed to connect to Unifi: %s', ex)
|
||||
persistent_notification.create(
|
||||
hass, 'Failed to connect to Unifi. '
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
return UnifiScanner(ctrl)
|
||||
|
||||
|
||||
class UnifiScanner(object):
|
||||
class UnifiScanner(DeviceScanner):
|
||||
"""Provide device_tracker support from Unifi WAP client data."""
|
||||
|
||||
def __init__(self, controller):
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
Support for UPC ConnectBox router.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.upc_connect/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_IP = '192.168.0.1'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string,
|
||||
})
|
||||
|
||||
CMD_LOGIN = 15
|
||||
CMD_DEVICES = 123
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_get_scanner(hass, config):
|
||||
"""Return the UPC device scanner."""
|
||||
scanner = UPCDeviceScanner(hass, config[DOMAIN])
|
||||
success_init = yield from scanner.async_login()
|
||||
|
||||
return scanner if success_init else None
|
||||
|
||||
|
||||
class UPCDeviceScanner(DeviceScanner):
|
||||
"""This class queries a router running UPC ConnectBox firmware."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize the scanner."""
|
||||
self.hass = hass
|
||||
self.host = config[CONF_HOST]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
|
||||
self.data = {}
|
||||
self.token = None
|
||||
|
||||
self.headers = {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Referer': "http://{}/index.html".format(self.host),
|
||||
'User-Agent': ("Mozilla/5.0 (Windows NT 10.0; WOW64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/47.0.2526.106 Safari/537.36")
|
||||
}
|
||||
|
||||
self.websession = async_create_clientsession(
|
||||
hass, cookie_jar=aiohttp.CookieJar(unsafe=True, loop=hass.loop))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
if self.token is None:
|
||||
reconnect = yield from self.async_login()
|
||||
if not reconnect:
|
||||
_LOGGER.error("Not connected to %s", self.host)
|
||||
return []
|
||||
|
||||
raw = yield from self._async_ws_function(CMD_DEVICES)
|
||||
xml_root = ET.fromstring(raw)
|
||||
|
||||
return [mac.text for mac in xml_root.iter('MACAddr')]
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_get_device_name(self, device):
|
||||
"""The firmware doesn't save the name of the wireless device."""
|
||||
return None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_login(self):
|
||||
"""Login into firmware and get first token."""
|
||||
response = None
|
||||
try:
|
||||
# get first token
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
response = yield from self.websession.get(
|
||||
"http://{}/common_page/login.html".format(self.host)
|
||||
)
|
||||
|
||||
self.token = self._async_get_token()
|
||||
|
||||
# login
|
||||
data = yield from self._async_ws_function(CMD_LOGIN, {
|
||||
'Username': 'NULL',
|
||||
'Password': self.password,
|
||||
})
|
||||
|
||||
# successfull?
|
||||
if data.find("successful") != -1:
|
||||
return True
|
||||
return False
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.error("Can not load login page from %s", self.host)
|
||||
return False
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.release()
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_ws_function(self, function, additional_form=None):
|
||||
"""Execute a command on UPC firmware webservice."""
|
||||
form_data = {
|
||||
'token': self.token,
|
||||
'fun': function
|
||||
}
|
||||
|
||||
if additional_form:
|
||||
form_data.update(additional_form)
|
||||
|
||||
response = None
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
response = yield from self.websession.post(
|
||||
"http://{}/xml/getter.xml".format(self.host),
|
||||
data=form_data,
|
||||
headers=self.headers
|
||||
)
|
||||
|
||||
# error on UPC webservice
|
||||
if response.status != 200:
|
||||
_LOGGER.warning(
|
||||
"Error %d on %s.", response.status, function)
|
||||
self.token = None
|
||||
return
|
||||
|
||||
# load data, store token for next request
|
||||
raw = yield from response.text()
|
||||
self.token = self._async_get_token()
|
||||
|
||||
return raw
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.error("Error on %s", function)
|
||||
self.token = None
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.release()
|
||||
|
||||
def _async_get_token(self):
|
||||
"""Extract token from cookies."""
|
||||
cookie_manager = self.websession.cookie_jar.filter_cookies(
|
||||
"http://{}".format(self.host))
|
||||
|
||||
return cookie_manager.get('sessionToken')
|
||||
@@ -14,12 +14,9 @@ from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_USERNAME)
|
||||
CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME)
|
||||
from homeassistant.components.device_tracker import (
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
PLATFORM_SCHEMA)
|
||||
DEFAULT_SCAN_INTERVAL, PLATFORM_SCHEMA)
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(minutes=1)
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Support for Xiaomi Mi routers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.xiaomi/
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME, default='admin'): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
})
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a Xiaomi Device Scanner."""
|
||||
scanner = XioamiDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class XioamiDeviceScanner(DeviceScanner):
|
||||
"""This class queries a Xiaomi Mi router.
|
||||
|
||||
Adapted from Luci scanner.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
host = config[CONF_HOST]
|
||||
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = {}
|
||||
self.token = _get_token(host, username, password)
|
||||
|
||||
self.host = host
|
||||
|
||||
self.mac2name = None
|
||||
self.success_init = self.token is not None
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
return self.last_results
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
with self.lock:
|
||||
if self.mac2name is None:
|
||||
url = "http://{}/cgi-bin/luci/;stok={}/api/misystem/devicelist"
|
||||
url = url.format(self.host, self.token)
|
||||
result = _get_device_list(url)
|
||||
if result:
|
||||
hosts = [x for x in result
|
||||
if 'mac' in x and 'name' in x]
|
||||
mac2name_list = [
|
||||
(x['mac'].upper(), x['name']) for x in hosts]
|
||||
self.mac2name = dict(mac2name_list)
|
||||
else:
|
||||
# Error, handled in the _req_json_rpc
|
||||
return
|
||||
return self.mac2name.get(device.upper(), None)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the informations from the router are up to date.
|
||||
|
||||
Returns true if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
_LOGGER.info('Refreshing device list')
|
||||
url = "http://{}/cgi-bin/luci/;stok={}/api/misystem/devicelist"
|
||||
url = url.format(self.host, self.token)
|
||||
result = _get_device_list(url)
|
||||
if result:
|
||||
self.last_results = []
|
||||
for device_entry in result:
|
||||
# Check if the device is marked as connected
|
||||
if int(device_entry['online']) == 1:
|
||||
self.last_results.append(device_entry['mac'])
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _get_device_list(url, **kwargs):
|
||||
try:
|
||||
res = requests.get(url, timeout=5, **kwargs)
|
||||
except requests.exceptions.Timeout:
|
||||
_LOGGER.exception('Connection to the router timed out')
|
||||
return
|
||||
return _extract_result(res, 'list')
|
||||
|
||||
|
||||
def _get_token(host, username, password):
|
||||
"""Get authentication token for the given host+username+password."""
|
||||
url = 'http://{}/cgi-bin/luci/api/xqsystem/login'.format(host)
|
||||
data = {'username': username, 'password': password}
|
||||
try:
|
||||
res = requests.post(url, data=data, timeout=5)
|
||||
except requests.exceptions.Timeout:
|
||||
_LOGGER.exception('Connection to the router timed out')
|
||||
return
|
||||
return _extract_result(res, 'token')
|
||||
|
||||
|
||||
def _extract_result(res, key_name):
|
||||
if res.status_code == 200:
|
||||
try:
|
||||
result = res.json()
|
||||
except ValueError:
|
||||
# If json decoder could not parse the response
|
||||
_LOGGER.exception('Failed to parse response from mi router')
|
||||
return
|
||||
try:
|
||||
return result[key_name]
|
||||
except KeyError:
|
||||
_LOGGER.exception('No %s in response from mi router. %s',
|
||||
key_name, result)
|
||||
return
|
||||
else:
|
||||
_LOGGER.error('Invalid response from mi router: %s', res)
|
||||
@@ -14,7 +14,7 @@ import voluptuous as vol
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.discovery import load_platform, discover
|
||||
|
||||
REQUIREMENTS = ['netdisco==0.7.7']
|
||||
REQUIREMENTS = ['netdisco==0.8.1']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
@@ -36,6 +36,9 @@ SERVICE_HANDLERS = {
|
||||
'yamaha': ('media_player', 'yamaha'),
|
||||
'logitech_mediaserver': ('media_player', 'squeezebox'),
|
||||
'directv': ('media_player', 'directv'),
|
||||
'denonavr': ('media_player', 'denonavr'),
|
||||
'samsung_tv': ('media_player', 'samsungtv'),
|
||||
'yeelight': ('light', 'yeelight'),
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
|
||||
@@ -6,12 +6,17 @@ from aiohttp import web
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
STATE_ON, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_SET,
|
||||
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, STATE_ON, STATE_OFF,
|
||||
HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
||||
)
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
|
||||
)
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_VOLUME_LEVEL, ATTR_SUPPORTED_MEDIA_COMMANDS,
|
||||
SUPPORT_VOLUME_SET,
|
||||
)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -65,8 +70,11 @@ class HueAllLightsStateView(HomeAssistantView):
|
||||
|
||||
for entity in hass.states.async_all():
|
||||
if self.config.is_entity_exposed(entity):
|
||||
state, brightness = get_entity_state(self.config, entity)
|
||||
|
||||
number = self.config.entity_id_to_number(entity.entity_id)
|
||||
json_response[number] = entity_to_json(entity)
|
||||
json_response[number] = entity_to_json(
|
||||
entity, state, brightness)
|
||||
|
||||
return self.json(json_response)
|
||||
|
||||
@@ -83,7 +91,7 @@ class HueOneLightStateView(HomeAssistantView):
|
||||
self.config = config
|
||||
|
||||
@core.callback
|
||||
def get(self, request, username, entity_id=None):
|
||||
def get(self, request, username, entity_id):
|
||||
"""Process a request to get the state of an individual light."""
|
||||
hass = request.app['hass']
|
||||
entity_id = self.config.number_to_entity_id(entity_id)
|
||||
@@ -97,16 +105,9 @@ class HueOneLightStateView(HomeAssistantView):
|
||||
_LOGGER.error('Entity not exposed: %s', entity_id)
|
||||
return web.Response(text="Entity not exposed", status=404)
|
||||
|
||||
cached_state = self.config.cached_states.get(entity_id, None)
|
||||
state, brightness = get_entity_state(self.config, entity)
|
||||
|
||||
if cached_state is None:
|
||||
final_state = entity.state == STATE_ON
|
||||
final_brightness = entity.attributes.get(
|
||||
ATTR_BRIGHTNESS, 255 if final_state else 0)
|
||||
else:
|
||||
final_state, final_brightness = cached_state
|
||||
|
||||
json_response = entity_to_json(entity, final_state, final_brightness)
|
||||
json_response = entity_to_json(entity, state, brightness)
|
||||
|
||||
return self.json(json_response)
|
||||
|
||||
@@ -158,14 +159,27 @@ class HueOneLightChangeView(HomeAssistantView):
|
||||
|
||||
result, brightness = parsed
|
||||
|
||||
# Choose general HA domain
|
||||
domain = core.DOMAIN
|
||||
|
||||
# Entity needs separate call to turn on
|
||||
turn_on_needed = False
|
||||
|
||||
# Convert the resulting "on" status into the service we need to call
|
||||
service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF
|
||||
|
||||
# Construct what we need to send to the service
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
|
||||
# Make sure the entity actually supports brightness
|
||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
|
||||
if brightness is not None:
|
||||
data[ATTR_BRIGHTNESS] = brightness
|
||||
|
||||
# If the requested entity is a script add some variables
|
||||
if entity.domain == "script":
|
||||
elif entity.domain == "script":
|
||||
data['variables'] = {
|
||||
'requested_state': STATE_ON if result else STATE_OFF
|
||||
}
|
||||
@@ -173,8 +187,25 @@ class HueOneLightChangeView(HomeAssistantView):
|
||||
if brightness is not None:
|
||||
data['variables']['requested_level'] = brightness
|
||||
|
||||
elif brightness is not None:
|
||||
data[ATTR_BRIGHTNESS] = brightness
|
||||
# If the requested entity is a media player, convert to volume
|
||||
elif entity.domain == "media_player":
|
||||
media_commands = entity.attributes.get(
|
||||
ATTR_SUPPORTED_MEDIA_COMMANDS, 0)
|
||||
if media_commands & SUPPORT_VOLUME_SET == SUPPORT_VOLUME_SET:
|
||||
if brightness is not None:
|
||||
turn_on_needed = True
|
||||
domain = entity.domain
|
||||
service = SERVICE_VOLUME_SET
|
||||
# Convert 0-100 to 0.0-1.0
|
||||
data[ATTR_MEDIA_VOLUME_LEVEL] = brightness / 100.0
|
||||
|
||||
# If the requested entity is a cover, convert to open_cover/close_cover
|
||||
elif entity.domain == "cover":
|
||||
domain = entity.domain
|
||||
if service == SERVICE_TURN_ON:
|
||||
service = SERVICE_OPEN_COVER
|
||||
else:
|
||||
service = SERVICE_CLOSE_COVER
|
||||
|
||||
if entity.domain in config.off_maps_to_on_domains:
|
||||
# Map the off command to on
|
||||
@@ -187,9 +218,14 @@ class HueOneLightChangeView(HomeAssistantView):
|
||||
# as the actual requested command.
|
||||
config.cached_states[entity_id] = (result, brightness)
|
||||
|
||||
# Perform the requested action
|
||||
yield from hass.services.async_call(core.DOMAIN, service, data,
|
||||
blocking=True)
|
||||
# Separate call to turn on needed
|
||||
if turn_on_needed:
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
core.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True))
|
||||
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
domain, service, data, blocking=True))
|
||||
|
||||
json_response = \
|
||||
[create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
|
||||
@@ -219,23 +255,23 @@ def parse_hue_api_put_light_body(request_json, entity):
|
||||
result = False
|
||||
|
||||
if HUE_API_STATE_BRI in request_json:
|
||||
try:
|
||||
# Clamp brightness from 0 to 255
|
||||
brightness = \
|
||||
max(0, min(int(request_json[HUE_API_STATE_BRI]), 255))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
# Make sure the entity actually supports brightness
|
||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
|
||||
try:
|
||||
# Clamp brightness from 0 to 255
|
||||
brightness = \
|
||||
max(0, min(int(request_json[HUE_API_STATE_BRI]), 255))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
report_brightness = True
|
||||
result = (brightness > 0)
|
||||
elif entity.domain.lower() == "script":
|
||||
# Convert 0-255 to 0-100
|
||||
level = int(request_json[HUE_API_STATE_BRI]) / 255 * 100
|
||||
|
||||
elif entity.domain == "script" or entity.domain == "media_player":
|
||||
# Convert 0-255 to 0-100
|
||||
level = brightness / 255 * 100
|
||||
brightness = round(level)
|
||||
report_brightness = True
|
||||
result = True
|
||||
@@ -243,16 +279,38 @@ def parse_hue_api_put_light_body(request_json, entity):
|
||||
return (result, brightness) if report_brightness else (result, None)
|
||||
|
||||
|
||||
def get_entity_state(config, entity):
|
||||
"""Retrieve and convert state and brightness values for an entity."""
|
||||
cached_state = config.cached_states.get(entity.entity_id, None)
|
||||
|
||||
if cached_state is None:
|
||||
final_state = entity.state != STATE_OFF
|
||||
final_brightness = entity.attributes.get(
|
||||
ATTR_BRIGHTNESS, 255 if final_state else 0)
|
||||
|
||||
# Make sure the entity actually supports brightness
|
||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
|
||||
pass
|
||||
|
||||
elif entity.domain == "media_player":
|
||||
level = entity.attributes.get(
|
||||
ATTR_MEDIA_VOLUME_LEVEL, 1.0 if final_state else 0.0)
|
||||
# Convert 0.0-1.0 to 0-255
|
||||
final_brightness = round(min(1.0, level) * 255)
|
||||
else:
|
||||
final_state, final_brightness = cached_state
|
||||
# Make sure brightness is valid
|
||||
if final_brightness is None:
|
||||
final_brightness = 255 if final_state else 0
|
||||
|
||||
return (final_state, final_brightness)
|
||||
|
||||
|
||||
def entity_to_json(entity, is_on=None, brightness=None):
|
||||
"""Convert an entity to its Hue bridge JSON representation."""
|
||||
if is_on is None:
|
||||
is_on = entity.state == STATE_ON
|
||||
|
||||
if brightness is None:
|
||||
brightness = 255 if is_on else 0
|
||||
|
||||
name = entity.attributes.get(
|
||||
ATTR_EMULATED_HUE_NAME, entity.attributes[ATTR_FRIENDLY_NAME])
|
||||
name = entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name)
|
||||
|
||||
return {
|
||||
'state':
|
||||
|
||||
@@ -74,6 +74,7 @@ CACHE-CONTROL: max-age=60
|
||||
EXT:
|
||||
LOCATION: http://{0}:{1}/description.xml
|
||||
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1
|
||||
hue-bridgeid: 1234
|
||||
ST: urn:schemas-upnp-org:device:basic:1
|
||||
USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components.discovery import load_platform
|
||||
|
||||
REQUIREMENTS = ['pyenvisalink==1.9', 'pydispatcher==2.0.5']
|
||||
REQUIREMENTS = ['pyenvisalink==2.0', 'pydispatcher==2.0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'envisalink'
|
||||
|
||||
@@ -4,6 +4,7 @@ Provides functionality to interact with fans.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/fan/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -21,7 +22,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
DOMAIN = 'fan'
|
||||
SCAN_INTERVAL = 30
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
GROUP_NAME_ALL_FANS = 'all fans'
|
||||
ENTITY_ID_ALL_FANS = group.ENTITY_ID_FORMAT.format(GROUP_NAME_ALL_FANS)
|
||||
@@ -32,9 +33,11 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
ATTR_SUPPORTED_FEATURES = 'supported_features'
|
||||
SUPPORT_SET_SPEED = 1
|
||||
SUPPORT_OSCILLATE = 2
|
||||
SUPPORT_DIRECTION = 4
|
||||
|
||||
SERVICE_SET_SPEED = 'set_speed'
|
||||
SERVICE_OSCILLATE = 'oscillate'
|
||||
SERVICE_SET_DIRECTION = 'set_direction'
|
||||
|
||||
SPEED_OFF = 'off'
|
||||
SPEED_LOW = 'low'
|
||||
@@ -42,15 +45,20 @@ SPEED_MED = 'med'
|
||||
SPEED_MEDIUM = 'medium'
|
||||
SPEED_HIGH = 'high'
|
||||
|
||||
DIRECTION_FORWARD = 'forward'
|
||||
DIRECTION_REVERSE = 'reverse'
|
||||
|
||||
ATTR_SPEED = 'speed'
|
||||
ATTR_SPEED_LIST = 'speed_list'
|
||||
ATTR_OSCILLATING = 'oscillating'
|
||||
ATTR_DIRECTION = 'direction'
|
||||
|
||||
PROP_TO_ATTR = {
|
||||
'speed': ATTR_SPEED,
|
||||
'speed_list': ATTR_SPEED_LIST,
|
||||
'oscillating': ATTR_OSCILLATING,
|
||||
'supported_features': ATTR_SUPPORTED_FEATURES,
|
||||
'direction': ATTR_DIRECTION,
|
||||
} # type: dict
|
||||
|
||||
FAN_SET_SPEED_SCHEMA = vol.Schema({
|
||||
@@ -76,6 +84,11 @@ FAN_TOGGLE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids
|
||||
})
|
||||
|
||||
FAN_SET_DIRECTION_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_DIRECTION): cv.string
|
||||
}) # type: dict
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -140,6 +153,18 @@ def set_speed(hass, entity_id: str=None, speed: str=None) -> None:
|
||||
hass.services.call(DOMAIN, SERVICE_SET_SPEED, data)
|
||||
|
||||
|
||||
def set_direction(hass, entity_id: str=None, direction: str=None) -> None:
|
||||
"""Set direction for all or specified fan."""
|
||||
data = {
|
||||
key: value for key, value in [
|
||||
(ATTR_ENTITY_ID, entity_id),
|
||||
(ATTR_DIRECTION, direction),
|
||||
] if value is not None
|
||||
}
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, data)
|
||||
|
||||
|
||||
def setup(hass, config: dict) -> None:
|
||||
"""Expose fan control via statemachine and services."""
|
||||
component = EntityComponent(
|
||||
@@ -157,7 +182,8 @@ def setup(hass, config: dict) -> None:
|
||||
|
||||
service_fun = None
|
||||
for service_def in [SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
SERVICE_SET_SPEED, SERVICE_OSCILLATE]:
|
||||
SERVICE_SET_SPEED, SERVICE_OSCILLATE,
|
||||
SERVICE_SET_DIRECTION]:
|
||||
if service_def == service.service:
|
||||
service_fun = service_def
|
||||
break
|
||||
@@ -190,6 +216,10 @@ def setup(hass, config: dict) -> None:
|
||||
descriptions.get(SERVICE_OSCILLATE),
|
||||
schema=FAN_OSCILLATE_SCHEMA)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SET_DIRECTION, handle_fan_service,
|
||||
descriptions.get(SERVICE_SET_DIRECTION),
|
||||
schema=FAN_SET_DIRECTION_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -200,7 +230,11 @@ class FanEntity(ToggleEntity):
|
||||
|
||||
def set_speed(self: ToggleEntity, speed: str) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_direction(self: ToggleEntity, direction: str) -> None:
|
||||
"""Set the direction of the fan."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
|
||||
"""Turn on the fan."""
|
||||
@@ -217,14 +251,23 @@ class FanEntity(ToggleEntity):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the entity is on."""
|
||||
return self.state_attributes.get(ATTR_SPEED, STATE_UNKNOWN) \
|
||||
not in [SPEED_OFF, STATE_UNKNOWN]
|
||||
return self.speed not in [SPEED_OFF, STATE_UNKNOWN]
|
||||
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
"""Return the current speed."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def speed_list(self: ToggleEntity) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
return []
|
||||
|
||||
@property
|
||||
def current_direction(self) -> str:
|
||||
"""Return the current direction of the fan."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def state_attributes(self: ToggleEntity) -> dict:
|
||||
"""Return optional state attributes."""
|
||||
|
||||
@@ -7,14 +7,14 @@ https://home-assistant.io/components/demo/
|
||||
|
||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_HIGH,
|
||||
FanEntity, SUPPORT_SET_SPEED,
|
||||
SUPPORT_OSCILLATE)
|
||||
SUPPORT_OSCILLATE, SUPPORT_DIRECTION)
|
||||
from homeassistant.const import STATE_OFF
|
||||
|
||||
|
||||
FAN_NAME = 'Living Room Fan'
|
||||
FAN_ENTITY_ID = 'fan.living_room_fan'
|
||||
|
||||
DEMO_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE
|
||||
DEMO_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@@ -31,8 +31,9 @@ class DemoFan(FanEntity):
|
||||
def __init__(self, hass, name: str, initial_state: str) -> None:
|
||||
"""Initialize the entity."""
|
||||
self.hass = hass
|
||||
self.speed = initial_state
|
||||
self._speed = initial_state
|
||||
self.oscillating = False
|
||||
self.direction = "forward"
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
@@ -45,6 +46,11 @@ class DemoFan(FanEntity):
|
||||
"""No polling needed for a demo fan."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
"""Return the current speed."""
|
||||
return self._speed
|
||||
|
||||
@property
|
||||
def speed_list(self) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
@@ -61,7 +67,12 @@ class DemoFan(FanEntity):
|
||||
|
||||
def set_speed(self, speed: str) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
self.speed = speed
|
||||
self._speed = speed
|
||||
self.update_ha_state()
|
||||
|
||||
def set_direction(self, direction: str) -> None:
|
||||
"""Set the direction of the fan."""
|
||||
self.direction = direction
|
||||
self.update_ha_state()
|
||||
|
||||
def oscillate(self, oscillating: bool) -> None:
|
||||
@@ -69,6 +80,11 @@ class DemoFan(FanEntity):
|
||||
self.oscillating = oscillating
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def current_direction(self) -> str:
|
||||
"""Fan direction."""
|
||||
return self.direction
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
|
||||
@@ -64,7 +64,11 @@ class ISYFanDevice(isy.ISYDevice, FanEntity):
|
||||
def __init__(self, node) -> None:
|
||||
"""Initialize the ISY994 fan device."""
|
||||
isy.ISYDevice.__init__(self, node)
|
||||
self.speed = self.state
|
||||
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
"""Return the current speed."""
|
||||
return self.state
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
Support for Wink fans.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/fan.wink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.fan import (FanEntity, SPEED_HIGH,
|
||||
SPEED_LOW, SPEED_MEDIUM,
|
||||
STATE_UNKNOWN)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SPEED_LOWEST = "lowest"
|
||||
SPEED_AUTO = "auto"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Wink platform."""
|
||||
import pywink
|
||||
|
||||
add_devices(WinkFanDevice(fan, hass) for fan in pywink.get_fans())
|
||||
|
||||
|
||||
class WinkFanDevice(WinkDevice, FanEntity):
|
||||
"""Representation of a Wink fan."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the fan."""
|
||||
WinkDevice.__init__(self, wink, hass)
|
||||
|
||||
def set_drection(self: ToggleEntity, direction: str) -> None:
|
||||
"""Set the direction of the fan."""
|
||||
self.wink.set_fan_direction(direction)
|
||||
|
||||
def set_speed(self: ToggleEntity, speed: str) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
self.wink.set_fan_speed(speed)
|
||||
|
||||
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
|
||||
"""Turn on the fan."""
|
||||
self.wink.set_state(True)
|
||||
|
||||
def turn_off(self: ToggleEntity, **kwargs) -> None:
|
||||
"""Turn off the fan."""
|
||||
self.wink.set_state(False)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the entity is on."""
|
||||
return self.wink.state()
|
||||
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
"""Return the current speed."""
|
||||
current_wink_speed = self.wink.current_fan_speed()
|
||||
if SPEED_AUTO == current_wink_speed:
|
||||
return SPEED_AUTO
|
||||
if SPEED_LOWEST == current_wink_speed:
|
||||
return SPEED_LOWEST
|
||||
if SPEED_LOW == current_wink_speed:
|
||||
return SPEED_LOW
|
||||
if SPEED_MEDIUM == current_wink_speed:
|
||||
return SPEED_MEDIUM
|
||||
if SPEED_HIGH == current_wink_speed:
|
||||
return SPEED_HIGH
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def current_direction(self):
|
||||
"""Return direction of the fan [forward, reverse]."""
|
||||
return self.wink.current_fan_direction()
|
||||
|
||||
@property
|
||||
def speed_list(self: ToggleEntity) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
wink_supported_speeds = self.wink.fan_speeds()
|
||||
supported_speeds = []
|
||||
if SPEED_AUTO in wink_supported_speeds:
|
||||
supported_speeds.append(SPEED_AUTO)
|
||||
if SPEED_LOWEST in wink_supported_speeds:
|
||||
supported_speeds.append(SPEED_LOWEST)
|
||||
if SPEED_LOW in wink_supported_speeds:
|
||||
supported_speeds.append(SPEED_LOW)
|
||||
if SPEED_MEDIUM in wink_supported_speeds:
|
||||
supported_speeds.append(SPEED_MEDIUM)
|
||||
if SPEED_HIGH in wink_supported_speeds:
|
||||
supported_speeds.append(SPEED_HIGH)
|
||||
return supported_speeds
|
||||
@@ -8,6 +8,7 @@
|
||||
<link rel='icon' href='/static/icons/favicon.ico'>
|
||||
<link rel='apple-touch-icon' sizes='180x180'
|
||||
href='/static/icons/favicon-apple-180x180.png'>
|
||||
<link rel="mask-icon" href="/static/icons/home-assistant-icon.svg" color="#3fbbf4">
|
||||
{% for panel in panels.values() -%}
|
||||
<link rel='prefetch' href='{{ panel.url }}'>
|
||||
{% endfor -%}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||
|
||||
FINGERPRINTS = {
|
||||
"core.js": "5dfb2d3e567fad37af0321d4b29265ed",
|
||||
"frontend.html": "6a89b74ab2b76c7d28fad2aea9444ec2",
|
||||
"mdi.html": "46a76f877ac9848899b8ed382427c16f",
|
||||
"core.js": "22d39af274e1d824ca1302e10971f2d8",
|
||||
"frontend.html": "61e57194179b27563a05282b58dd4f47",
|
||||
"mdi.html": "5bb2f1717206bad0d187c2633062c575",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-dev-event.html": "c2d5ec676be98d4474d19f94d0262c1e",
|
||||
"panels/ha-panel-dev-info.html": "a9c07bf281fe9791fb15827ec1286825",
|
||||
"panels/ha-panel-dev-service.html": "b3fe49532c5c03198fafb0c6ed58b76a",
|
||||
"panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054",
|
||||
"panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400",
|
||||
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
|
||||
"panels/ha-panel-dev-event.html": "f19840b9a6a46f57cb064b384e1353f5",
|
||||
"panels/ha-panel-dev-info.html": "3765a371478cc66d677cf6dcc35267c6",
|
||||
"panels/ha-panel-dev-service.html": "e32bcd3afdf485417a3e20b4fc760776",
|
||||
"panels/ha-panel-dev-state.html": "8257d99a38358a150eafdb23fa6727e0",
|
||||
"panels/ha-panel-dev-template.html": "cbb251acabd5e7431058ed507b70522b",
|
||||
"panels/ha-panel-history.html": "7baeb4bd7d9ce0def4f95eab6f10812e",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "4bc5c8370a85a4215413fbae8f85addb",
|
||||
"panels/ha-panel-map.html": "1bf6965b24d76db71a1871865cd4a3a2",
|
||||
"panels/ha-panel-logbook.html": "93de4cee3a2352a6813b5c218421d534",
|
||||
"panels/ha-panel-map.html": "3b0ca63286cbe80f27bd36dbc2434e89",
|
||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user