Compare commits
405 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d1dce2519 | |||
| 01d097b9b0 | |||
| 7f60f1e662 | |||
| bc6c285945 | |||
| d4d8c9ae65 | |||
| 9292891836 | |||
| 035df68d6c | |||
| c611be96ad | |||
| d46720ee2c | |||
| c1f464f478 | |||
| 8c5759e460 | |||
| 8490d6126a | |||
| 8b5b580287 | |||
| d35f5b9f97 | |||
| 6de64d7695 | |||
| 7f1da8b7bc | |||
| 58ac4be24c | |||
| b2919c6504 | |||
| 3863d2985a | |||
| 0180c056e1 | |||
| e6cd9a6dc7 | |||
| 33028dd143 | |||
| 9bdfa89b7c | |||
| 42b80868d4 | |||
| 707ca4b752 | |||
| c7d2a09097 | |||
| fb9f83f8ad | |||
| 52ebb2fb3b | |||
| ea7ca48ba2 | |||
| ab80af099c | |||
| 34531895a0 | |||
| 645cd89406 | |||
| cc5217d818 | |||
| e454806669 | |||
| 726557b2f6 | |||
| c7e22e6910 | |||
| f66a020bfc | |||
| 64a73f6b67 | |||
| ad7f034805 | |||
| 76674d4de9 | |||
| 0dc9f2a9f8 | |||
| ce47b58a8b | |||
| 5d71d5560e | |||
| 1dc9bfdf73 | |||
| 2eb36c18bd | |||
| 5ad27d8cdb | |||
| 68c2b539ee | |||
| 755234369d | |||
| 0a34e8de02 | |||
| 52ed25fc21 | |||
| 9e866680d4 | |||
| 80c89d218b | |||
| 4f1bf7b2bf | |||
| e557e355db | |||
| e7320fe969 | |||
| 4e3bd5f2a9 | |||
| 9a6b2c1831 | |||
| ca0b6ebd99 | |||
| 47cd0b20a0 | |||
| 98d051f870 | |||
| 5f98705100 | |||
| 63bf4db969 | |||
| 3ec00ce4fe | |||
| 74a0e47ba6 | |||
| 476e4f0517 | |||
| 61fb8271e5 | |||
| 5cf9bd7223 | |||
| 9f986c55e6 | |||
| 94eb54ff00 | |||
| f28b392f1a | |||
| fa71d5fac9 | |||
| 21fd53b05d | |||
| b0b3c2f73f | |||
| d660d2b3dc | |||
| a89bfcf342 | |||
| a42347e6e7 | |||
| faee3e8447 | |||
| 3625646c34 | |||
| 5a562f3db8 | |||
| 19705ab40a | |||
| 20bf9f7ea1 | |||
| 6399c873f9 | |||
| 4be1053f1c | |||
| efdd0c9e8a | |||
| 8d42e42230 | |||
| 4b6878f91c | |||
| 90f35b35cd | |||
| 082920abe0 | |||
| 4a8bbc52e0 | |||
| 1674c8309a | |||
| 62f016e7d2 | |||
| 34e5ecb8ab | |||
| 8f95885e3a | |||
| 94db1ac142 | |||
| f48e65096a | |||
| 3244975489 | |||
| 10327795e9 | |||
| bcbb8edd59 | |||
| 86270e1a37 | |||
| de7a34b648 | |||
| 82a06279de | |||
| 62af1fcc57 | |||
| 6afe99dcc7 | |||
| b6bf398859 | |||
| 48df06d1c0 | |||
| b4ca691822 | |||
| 16c2827465 | |||
| e90fd3d654 | |||
| 7d0ff6884c | |||
| a9ea8972dd | |||
| a0c1202ad6 | |||
| 1bf45c8f33 | |||
| c5094438de | |||
| 60d45ebf79 | |||
| 5df2a1cf76 | |||
| f5b2fa6fbe | |||
| 03b2ced24e | |||
| 6c18f264f3 | |||
| cdc371c3ee | |||
| 826b3be087 | |||
| f5000d401b | |||
| 7443f4faf8 | |||
| 3158db9553 | |||
| 46a0173e31 | |||
| 7e511bcacf | |||
| d7fd2ccdaf | |||
| b2999ae325 | |||
| 5033c1fcb7 | |||
| e492be299b | |||
| 03e7281406 | |||
| 16d75b2981 | |||
| cc7784889a | |||
| d267f0a04c | |||
| a8e0ca6d3f | |||
| f8175adbdc | |||
| 6437f6f6b4 | |||
| 27bbfbae62 | |||
| 2785c373fb | |||
| acddae3747 | |||
| d3e9a22759 | |||
| ca698ff063 | |||
| a866d515f7 | |||
| 3af4f267b3 | |||
| bd61555698 | |||
| 5027acfda1 | |||
| f0991d63d1 | |||
| 34f36479c6 | |||
| 506c88dbaf | |||
| 98a1addc18 | |||
| 30492cc685 | |||
| 0d09e2e1df | |||
| 81085c7467 | |||
| 19d40612e6 | |||
| 48306ddbf6 | |||
| a60a9202a5 | |||
| ab81231e6d | |||
| 68286dcef8 | |||
| 6e96f915f6 | |||
| 620a7eadf4 | |||
| 2332548cf4 | |||
| dc55525206 | |||
| 9318b36ac2 | |||
| 46f6653183 | |||
| 9736761968 | |||
| 6352f10d9e | |||
| 2a3b911d7b | |||
| 720e5876a7 | |||
| 85489010bc | |||
| 60d8266ce0 | |||
| e29deb0202 | |||
| 268b0f17d0 | |||
| 49ce85f2e4 | |||
| 1771f8b1b3 | |||
| 8cd1c42e80 | |||
| ec1d5e617e | |||
| 40651ef2bc | |||
| 64741a95b8 | |||
| a24b38aacc | |||
| 55f6ff86e4 | |||
| 79cdda2bd9 | |||
| 35eed93443 | |||
| dd4e1cbd1d | |||
| 2084976bc2 | |||
| 9019d654d7 | |||
| e0f6239ef3 | |||
| b9e1b3eb99 | |||
| e1a7b8f988 | |||
| be9cfbdeb0 | |||
| a32229b4ce | |||
| 7da104af4e | |||
| 9d7aef94e0 | |||
| e4c5108c9d | |||
| fc946da5db | |||
| b33714bca3 | |||
| 722af9014d | |||
| 6abaebb248 | |||
| c01e9bea2b | |||
| 5ce4ade737 | |||
| 9ce8f385d2 | |||
| d7464aea86 | |||
| 071952462c | |||
| ab79b8a541 | |||
| 3a3374ed4b | |||
| 4d53fa0173 | |||
| 5369d8c61c | |||
| 6c1f44242c | |||
| 4371355be1 | |||
| 5bb88909a0 | |||
| 4b0c416844 | |||
| 737d7c9d22 | |||
| 15be5ced9a | |||
| 7c549db2d6 | |||
| 6e6aa15f7c | |||
| e0c1885a71 | |||
| 049cd159ce | |||
| 95e05d4fc9 | |||
| bf14067eb0 | |||
| 8c77418b6a | |||
| d25a42426a | |||
| 3ed102cd88 | |||
| 90e2aefd23 | |||
| 47af247d6a | |||
| 3947ed3c2b | |||
| 1a00d4a095 | |||
| ccecc0181d | |||
| e90dbad37e | |||
| 8ec0c36457 | |||
| e68cc83e64 | |||
| 4ad4d74ed4 | |||
| 550f31d4c3 | |||
| 7e42b35b62 | |||
| 9b96471182 | |||
| 3c3eadbef5 | |||
| f375bc527a | |||
| 6de04d78ed | |||
| 86aea83f64 | |||
| 98feb3cd93 | |||
| 5af1643297 | |||
| 3dcd18af9e | |||
| 2fd7b98cab | |||
| 90e21791f6 | |||
| 9678613a13 | |||
| 5de89316b2 | |||
| 95eabe7c0e | |||
| 9bec0316ea | |||
| 61685ea13d | |||
| 77b9a12687 | |||
| 08f2a67de4 | |||
| 58c3b03b79 | |||
| c18294ee76 | |||
| 408f0cff78 | |||
| 0584c10ef9 | |||
| ae527e9c6f | |||
| 1ec5178f66 | |||
| 2978e0dabe | |||
| e26f0f7b7d | |||
| 2ff2a78e97 | |||
| bb172d8c98 | |||
| acb288f9e7 | |||
| c7565baa6d | |||
| fb29611c15 | |||
| 37cd62447e | |||
| 5cbcd72912 | |||
| 8bba0b88fd | |||
| b1f17c2cd4 | |||
| 8017f7f241 | |||
| 1a73c1b991 | |||
| 039c5cd847 | |||
| 3b27bef1ac | |||
| 1fc2204ca9 | |||
| 834ce5269d | |||
| 9ada5e6b2b | |||
| f17ef0327c | |||
| 56a151b196 | |||
| 6e927d68e5 | |||
| fcad068016 | |||
| e12cc2fbbf | |||
| 9588fcc5cc | |||
| a4aa2e4383 | |||
| fe074835f0 | |||
| b2ad8db86b | |||
| 20f021d05f | |||
| fc43135ddd | |||
| e86ee9eae7 | |||
| 332f7621ce | |||
| 68c1dd7cd4 | |||
| fe2a9bb83e | |||
| 2f8591205f | |||
| 65c3184856 | |||
| 0afb6114c5 | |||
| 7c7b6ca05c | |||
| 2fe8b154f1 | |||
| bf64956265 | |||
| eb11486e76 | |||
| e8c3eaab33 | |||
| fcbeddeb57 | |||
| 50b23e1969 | |||
| 984f01359c | |||
| d5198d4242 | |||
| f5d1da1d53 | |||
| 13ca42e187 | |||
| 74eb577c58 | |||
| fe7134b897 | |||
| 27845d3fc5 | |||
| 6606d2a73c | |||
| 6dc877d8de | |||
| b0441aadc4 | |||
| dd71e4fdd1 | |||
| 7e066e11ad | |||
| 13d40fe6ec | |||
| 7e75add144 | |||
| 2df26a0d1a | |||
| 965730eb60 | |||
| 4c0ac6051f | |||
| 2a11d02fe4 | |||
| 046c5653cb | |||
| f86fcdcaf5 | |||
| 835bc1c492 | |||
| de5a2fee83 | |||
| 209499e82b | |||
| 9b47241a46 | |||
| 513f6e9c3c | |||
| 9582eae48e | |||
| d4834ff408 | |||
| ce22f3c82d | |||
| 246184507c | |||
| 3f3b475d76 | |||
| 40aa661340 | |||
| 6c3a78df30 | |||
| 964a1f9aef | |||
| 8360ab265c | |||
| e3dcb45879 | |||
| 683a80f5f4 | |||
| 9904727cde | |||
| e9da02d70c | |||
| b0b88e606c | |||
| 57a833f1a7 | |||
| e5e577108c | |||
| 51dd718282 | |||
| 40340ea832 | |||
| a2ca60159d | |||
| 50f5f1860c | |||
| 8e89308a15 | |||
| 96cfff192a | |||
| 067993c8ab | |||
| eef1e65244 | |||
| 134c870d2b | |||
| 5edc4f148f | |||
| 880b5f0ad1 | |||
| 804b7669b7 | |||
| 81288cc988 | |||
| d4174f5e42 | |||
| cfc23b0091 | |||
| bb42e264cb | |||
| c9bccadc40 | |||
| ab6cb43d5b | |||
| 4fa379419d | |||
| b01ff81b47 | |||
| 6dcb87c54d | |||
| 6cfca09daf | |||
| 4eba1250e9 | |||
| d4d798d71f | |||
| 3dc1dc6c6a | |||
| 473047f3dd | |||
| f5b5d3f65a | |||
| 776c7dae07 | |||
| 4ccedca3e5 | |||
| d9b97ad5b4 | |||
| de89de890f | |||
| 5338b29edf | |||
| 395dbe8804 | |||
| f41786d893 | |||
| 34dee0c134 | |||
| 705238eb78 | |||
| 2b6e0da405 | |||
| 9d750368ff | |||
| 7252861b83 | |||
| db2140782f | |||
| b9f5ec9e2c | |||
| 6d9b618f1c | |||
| a459368998 | |||
| cb3f14a862 | |||
| e9367d5369 | |||
| c3dd94ba04 | |||
| 6624cfefd6 | |||
| 350ed9f764 | |||
| 3679a8078a | |||
| a25f7eed2b | |||
| ae058b7847 | |||
| aa74c4e57a | |||
| 1b874c603b | |||
| 050fe809e1 | |||
| e2b02f2fd2 | |||
| 775d3198ae | |||
| 21812ba717 | |||
| 2d54fdd979 | |||
| e093abc366 | |||
| 5d3e929599 | |||
| 1ec392a494 | |||
| d719dd72fe | |||
| 53b43dc4db | |||
| f21d97d5a2 | |||
| f9b17ab026 | |||
| e88fabbe6d | |||
| 0509b478e9 |
@@ -46,9 +46,12 @@ omit =
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/media_player/plex.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/notify/file.py
|
||||
homeassistant/components/notify/instapush.py
|
||||
homeassistant/components/notify/nma.py
|
||||
@@ -60,11 +63,14 @@ omit =
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/bitcoin.py
|
||||
homeassistant/components/sensor/command_sensor.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/efergy.py
|
||||
homeassistant/components/sensor/forecast.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/mysensors.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/rest.py
|
||||
homeassistant/components/sensor/rfxtrx.py
|
||||
homeassistant/components/sensor/rpi_gpio.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
@@ -73,6 +79,8 @@ omit =
|
||||
homeassistant/components/sensor/temper.py
|
||||
homeassistant/components/sensor/time_date.py
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/worldclock.py
|
||||
homeassistant/components/switch/arest.py
|
||||
homeassistant/components/switch/command_switch.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
|
||||
+3
-4
@@ -15,10 +15,6 @@ tests/config/home-assistant.log
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
# Hide code validator output
|
||||
pep8.txt
|
||||
pylint.txt
|
||||
|
||||
# Hide some OS X stuff
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
@@ -30,6 +26,9 @@ Icon
|
||||
|
||||
.idea
|
||||
|
||||
# pytest
|
||||
.cache
|
||||
|
||||
# GITHUB Proposed Python stuff:
|
||||
*.py[cod]
|
||||
|
||||
|
||||
+5
-7
@@ -1,13 +1,11 @@
|
||||
sudo: false
|
||||
language: python
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/virtualenv/python3.4.2/
|
||||
python:
|
||||
- "3.4"
|
||||
install:
|
||||
- pip install -r requirements_all.txt
|
||||
- pip install flake8 pylint coveralls
|
||||
- script/bootstrap_server
|
||||
script:
|
||||
- flake8 homeassistant
|
||||
- pylint homeassistant
|
||||
- coverage run -m unittest discover tests
|
||||
after_success:
|
||||
- coveralls
|
||||
- script/cibuild
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ For help on building your component, please see the [developer documentation](ht
|
||||
After you finish adding support for your device:
|
||||
|
||||
- Update the supported devices in the `README.md` file.
|
||||
- Add any new dependencies to `requirements.txt`.
|
||||
- Add any new dependencies to `requirements_all.txt`. There is no ordering right now, so just add it to the end.
|
||||
- Update the `.coveragerc` file.
|
||||
- Provide some documentation for [home-assistant.io](https://home-assistant.io/). The documentation is handled in a separate [git repository](https://github.com/balloob/home-assistant.io).
|
||||
- Make sure all your code passes Pylint and flake8 (PEP8 and some more) validation. To generate reports, run `pylint homeassistant > pylint.txt` and `flake8 homeassistant --exclude bower_components,external > flake8.txt`.
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
recursive-exclude tests *
|
||||
recursive-include homeassistant services.yaml
|
||||
|
||||
@@ -18,7 +18,7 @@ Examples of devices it can interface it:
|
||||
|
||||
* Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), and [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/)
|
||||
* [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, [Edimax](http://www.edimax.com/) switches, [Efergy](https://efergy.com) energy monitoring, RFXtrx sensors, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors
|
||||
* [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), and [Kodi (XBMC)](http://kodi.tv/)
|
||||
* [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), [Kodi (XBMC)](http://kodi.tv/), and iTunes (by way of [itunes-api](https://github.com/maddox/itunes-api))
|
||||
* Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), [Arduino](https://www.arduino.cc/), [Raspberry Pi](https://www.raspberrypi.org/), and [Modbus](http://www.modbus.org/)
|
||||
* Integrate data from the [Bitcoin](https://bitcoin.org) network, meteorological data from [OpenWeatherMap](http://openweathermap.org/) and [Forecast.io](https://forecast.io/), [Transmission](http://www.transmissionbt.com/), or [SABnzbd](http://sabnzbd.org).
|
||||
* [See full list of supported devices](https://home-assistant.io/components/)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
homeassistant:
|
||||
# Omitted values in this section will be auto detected using freegeoip.net
|
||||
|
||||
# Location required to calculate the time the sun rises and sets
|
||||
# Location required to calculate the time the sun rises and sets.
|
||||
# Cooridinates are also used for location for weather related components.
|
||||
# Google Maps can be used to determine more precise GPS cooridinates.
|
||||
latitude: 32.87336
|
||||
longitude: 117.22743
|
||||
|
||||
@@ -68,11 +70,18 @@ device_sun_light_trigger:
|
||||
|
||||
# A comma separated list of states that have to be tracked as a single group
|
||||
# Grouped states should share the same type of states (ON/OFF or HOME/NOT_HOME)
|
||||
# You can also have groups within groups.
|
||||
group:
|
||||
Home:
|
||||
- group.living_room
|
||||
- group.kitchen
|
||||
living_room:
|
||||
- light.Bowl
|
||||
- light.Ceiling
|
||||
- light.TV_back_light
|
||||
kitchen:
|
||||
- light.fan_bulb_1
|
||||
- light.fan_bulb_2
|
||||
children:
|
||||
- device_tracker.child_1
|
||||
- device_tracker.child_2
|
||||
@@ -94,28 +103,36 @@ browser:
|
||||
keyboard:
|
||||
|
||||
automation:
|
||||
platform: state
|
||||
alias: Sun starts shining
|
||||
- alias: 'Rule 1 Light on in the evening'
|
||||
trigger:
|
||||
- platform: sun
|
||||
event: sunset
|
||||
offset: "-01:00:00"
|
||||
- platform: state
|
||||
entity_id: group.all_devices
|
||||
state: home
|
||||
condition:
|
||||
- platform: state
|
||||
entity_id: group.all_devices
|
||||
state: home
|
||||
- platform: time
|
||||
after: "16:00:00"
|
||||
before: "23:00:00"
|
||||
action:
|
||||
service: homeassistant.turn_on
|
||||
entity_id: group.living_room
|
||||
|
||||
state_entity_id: sun.sun
|
||||
# Next two are optional, omit to match all
|
||||
state_from: below_horizon
|
||||
state_to: above_horizon
|
||||
- alias: 'Rule 2 - Away Mode'
|
||||
|
||||
execute_service: light.turn_off
|
||||
service_entity_id: group.living_room
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: group.all_devices
|
||||
state: 'not_home'
|
||||
|
||||
automation 2:
|
||||
platform: time
|
||||
alias: Beer o Clock
|
||||
|
||||
time_hours: 16
|
||||
time_minutes: 0
|
||||
time_seconds: 0
|
||||
|
||||
execute_service: notify.notify
|
||||
service_data:
|
||||
message: It's 4, time for beer!
|
||||
condition: use_trigger_values
|
||||
action:
|
||||
service: light.turn_off
|
||||
entity_id: group.all_lights
|
||||
|
||||
sensor:
|
||||
platform: systemmonitor
|
||||
@@ -135,6 +152,23 @@ sensor:
|
||||
- type: 'process'
|
||||
arg: 'octave-cli'
|
||||
|
||||
sensor 2:
|
||||
platform: forecast
|
||||
api_key: <register on Forecast.io for your PRIVATE API>
|
||||
monitored_conditions:
|
||||
- summary
|
||||
- precip_type
|
||||
- precip_intensity
|
||||
- temperature
|
||||
- dew_point
|
||||
- wind_speed
|
||||
- wind_bearing
|
||||
- cloud_cover
|
||||
- humidity
|
||||
- pressure
|
||||
- visibility
|
||||
- ozone
|
||||
|
||||
script:
|
||||
# Turns on the bedroom lights and then the living room lights 1 minute later
|
||||
wakeup:
|
||||
|
||||
@@ -95,6 +95,18 @@ def get_arguments():
|
||||
type=int,
|
||||
default=None,
|
||||
help='Enables daily log rotation and keeps up to the specified days')
|
||||
parser.add_argument(
|
||||
'--install-osx',
|
||||
action='store_true',
|
||||
help='Installs as a service on OS X and loads on boot.')
|
||||
parser.add_argument(
|
||||
'--uninstall-osx',
|
||||
action='store_true',
|
||||
help='Uninstalls from OS X.')
|
||||
parser.add_argument(
|
||||
'--restart-osx',
|
||||
action='store_true',
|
||||
help='Restarts on OS X.')
|
||||
if os.name != "nt":
|
||||
parser.add_argument(
|
||||
'--daemon',
|
||||
@@ -152,6 +164,46 @@ def write_pid(pid_file):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def install_osx():
|
||||
""" Setup to run via launchd on OS X """
|
||||
with os.popen('which hass') as inp:
|
||||
hass_path = inp.read().strip()
|
||||
|
||||
with os.popen('whoami') as inp:
|
||||
user = inp.read().strip()
|
||||
|
||||
cwd = os.path.dirname(__file__)
|
||||
template_path = os.path.join(cwd, 'startup', 'launchd.plist')
|
||||
|
||||
with open(template_path, 'r', encoding='utf-8') as inp:
|
||||
plist = inp.read()
|
||||
|
||||
plist = plist.replace("$HASS_PATH$", hass_path)
|
||||
plist = plist.replace("$USER$", user)
|
||||
|
||||
path = os.path.expanduser("~/Library/LaunchAgents/org.homeassistant.plist")
|
||||
|
||||
try:
|
||||
with open(path, 'w', encoding='utf-8') as outp:
|
||||
outp.write(plist)
|
||||
except IOError as err:
|
||||
print('Unable to write to ' + path, err)
|
||||
return
|
||||
|
||||
os.popen('launchctl load -w -F ' + path)
|
||||
|
||||
print("Home Assistant has been installed. \
|
||||
Open it here: http://localhost:8123")
|
||||
|
||||
|
||||
def uninstall_osx():
|
||||
""" Unload from launchd on OS X """
|
||||
path = os.path.expanduser("~/Library/LaunchAgents/org.homeassistant.plist")
|
||||
os.popen('launchctl unload ' + path)
|
||||
|
||||
print("Home Assistant has been uninstalled.")
|
||||
|
||||
|
||||
def main():
|
||||
""" Starts Home Assistant. """
|
||||
validate_python()
|
||||
@@ -161,6 +213,18 @@ def main():
|
||||
config_dir = os.path.join(os.getcwd(), args.config)
|
||||
ensure_config_path(config_dir)
|
||||
|
||||
# os x launchd functions
|
||||
if args.install_osx:
|
||||
install_osx()
|
||||
return
|
||||
if args.uninstall_osx:
|
||||
uninstall_osx()
|
||||
return
|
||||
if args.restart_osx:
|
||||
uninstall_osx()
|
||||
install_osx()
|
||||
return
|
||||
|
||||
# daemon functions
|
||||
if args.pid_file:
|
||||
check_pid(args.pid_file)
|
||||
|
||||
@@ -123,6 +123,7 @@ def prepare_setup_platform(hass, config, domain, platform_name):
|
||||
|
||||
# Not found
|
||||
if platform is None:
|
||||
_LOGGER.error('Unable to find platform %s', platform_path)
|
||||
return None
|
||||
|
||||
# Already loaded
|
||||
@@ -185,8 +186,8 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
dict, {key: value or {} for key, value in config.items()})
|
||||
|
||||
# Filter out the repeating and common config section [homeassistant]
|
||||
components = (key for key in config.keys()
|
||||
if ' ' not in key and key != core.DOMAIN)
|
||||
components = set(key.split(' ')[0] for key in config.keys()
|
||||
if key != core.DOMAIN)
|
||||
|
||||
if not core_components.setup(hass, config):
|
||||
_LOGGER.error('Home Assistant core failed to initialize. '
|
||||
@@ -296,11 +297,15 @@ def process_ha_core_config(hass, config):
|
||||
else:
|
||||
_LOGGER.error('Received invalid time zone %s', time_zone_str)
|
||||
|
||||
for key, attr in ((CONF_LATITUDE, 'latitude'),
|
||||
(CONF_LONGITUDE, 'longitude'),
|
||||
(CONF_NAME, 'location_name')):
|
||||
for key, attr, typ in ((CONF_LATITUDE, 'latitude', float),
|
||||
(CONF_LONGITUDE, 'longitude', float),
|
||||
(CONF_NAME, 'location_name', str)):
|
||||
if key in config:
|
||||
setattr(hac, attr, config[key])
|
||||
try:
|
||||
setattr(hac, attr, typ(config[key]))
|
||||
except ValueError:
|
||||
_LOGGER.error('Received invalid %s value for %s: %s',
|
||||
typ.__name__, key, attr)
|
||||
|
||||
set_time_zone(config.get(CONF_TIME_ZONE))
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
homeassistant.components.alarm_control_panel
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Component to interface with a alarm control panel.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.components import verisure
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
DOMAIN = 'alarm_control_panel'
|
||||
DEPENDENCIES = []
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
verisure.DISCOVER_SENSORS: 'verisure'
|
||||
}
|
||||
|
||||
SERVICE_TO_METHOD = {
|
||||
SERVICE_ALARM_DISARM: 'alarm_disarm',
|
||||
SERVICE_ALARM_ARM_HOME: 'alarm_arm_home',
|
||||
SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away',
|
||||
}
|
||||
|
||||
ATTR_CODE = 'code'
|
||||
ATTR_CODE_FORMAT = 'code_format'
|
||||
|
||||
ATTR_TO_PROPERTY = [
|
||||
ATTR_CODE,
|
||||
ATTR_CODE_FORMAT
|
||||
]
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Track states and offer events for sensors. """
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||
DISCOVERY_PLATFORMS)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
def alarm_service_handler(service):
|
||||
""" Maps services to methods on Alarm. """
|
||||
target_alarms = component.extract_from_service(service)
|
||||
|
||||
if ATTR_CODE not in service.data:
|
||||
return
|
||||
|
||||
code = service.data[ATTR_CODE]
|
||||
|
||||
method = SERVICE_TO_METHOD[service.service]
|
||||
|
||||
for alarm in target_alarms:
|
||||
getattr(alarm, method)(code)
|
||||
|
||||
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))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def alarm_disarm(hass, code, entity_id=None):
|
||||
""" Send the alarm the command for disarm. """
|
||||
data = {ATTR_CODE: code}
|
||||
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data)
|
||||
|
||||
|
||||
def alarm_arm_home(hass, code, entity_id=None):
|
||||
""" Send the alarm the command for arm home. """
|
||||
data = {ATTR_CODE: code}
|
||||
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data)
|
||||
|
||||
|
||||
def alarm_arm_away(hass, code, entity_id=None):
|
||||
""" Send the alarm the command for arm away. """
|
||||
data = {ATTR_CODE: code}
|
||||
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data)
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class AlarmControlPanel(Entity):
|
||||
""" ABC for alarm control devices. """
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" regex for code format or None if no code is required. """
|
||||
return None
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Return the state attributes. """
|
||||
state_attr = {
|
||||
ATTR_CODE_FORMAT: self.code_format,
|
||||
}
|
||||
return state_attr
|
||||
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
homeassistant.components.alarm_control_panel.mqtt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This platform enables the possibility to control a MQTT alarm.
|
||||
In this platform, 'state_topic' and 'command_topic' are required.
|
||||
The alarm will only change state after receiving the a new state
|
||||
from 'state_topic'. If these messages are published with RETAIN flag,
|
||||
the MQTT alarm will receive an instant state update after subscription
|
||||
and will start with correct state. Otherwise, the initial state will
|
||||
be 'unknown'.
|
||||
|
||||
Configuration:
|
||||
|
||||
alarm_control_panel:
|
||||
platform: mqtt
|
||||
name: "MQTT Alarm"
|
||||
state_topic: "home/alarm"
|
||||
command_topic: "home/alarm/set"
|
||||
qos: 0
|
||||
payload_disarm: "DISARM"
|
||||
payload_arm_home: "ARM_HOME"
|
||||
payload_arm_away: "ARM_AWAY"
|
||||
code: "mySecretCode"
|
||||
|
||||
Variables:
|
||||
|
||||
name
|
||||
*Optional
|
||||
The name of the alarm. Default is 'MQTT Alarm'.
|
||||
|
||||
state_topic
|
||||
*Required
|
||||
The MQTT topic subscribed to receive state updates.
|
||||
|
||||
command_topic
|
||||
*Required
|
||||
The MQTT topic to publish commands to change the alarm state.
|
||||
|
||||
qos
|
||||
*Optional
|
||||
The maximum QoS level of the state topic. Default is 0.
|
||||
This QoS will also be used to publishing messages.
|
||||
|
||||
payload_disarm
|
||||
*Optional
|
||||
The payload do disarm alarm. Default is "DISARM".
|
||||
|
||||
payload_arm_home
|
||||
*Optional
|
||||
The payload to set armed-home mode. Default is "ARM_HOME".
|
||||
|
||||
payload_arm_away
|
||||
*Optional
|
||||
The payload to set armed-away mode. Default is "ARM_AWAY".
|
||||
|
||||
code
|
||||
*Optional
|
||||
If defined, specifies a code to enable or disable the alarm in the frontend.
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
|
||||
from homeassistant.const import (STATE_UNKNOWN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "MQTT Alarm"
|
||||
DEFAULT_QOS = 0
|
||||
DEFAULT_PAYLOAD_DISARM = "DISARM"
|
||||
DEFAULT_PAYLOAD_ARM_HOME = "ARM_HOME"
|
||||
DEFAULT_PAYLOAD_ARM_AWAY = "ARM_AWAY"
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the MQTT platform. """
|
||||
|
||||
if config.get('state_topic') is None:
|
||||
_LOGGER.error("Missing required variable: state_topic")
|
||||
return False
|
||||
|
||||
if config.get('command_topic') is None:
|
||||
_LOGGER.error("Missing required variable: command_topic")
|
||||
return False
|
||||
|
||||
add_devices([MqttAlarm(
|
||||
hass,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get('state_topic'),
|
||||
config.get('command_topic'),
|
||||
config.get('qos', DEFAULT_QOS),
|
||||
config.get('payload_disarm', DEFAULT_PAYLOAD_DISARM),
|
||||
config.get('payload_arm_home', DEFAULT_PAYLOAD_ARM_HOME),
|
||||
config.get('payload_arm_away', DEFAULT_PAYLOAD_ARM_AWAY),
|
||||
config.get('code'))])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class MqttAlarm(alarm.AlarmControlPanel):
|
||||
""" represents a MQTT alarm status within home assistant. """
|
||||
|
||||
def __init__(self, hass, name, state_topic, command_topic, qos,
|
||||
payload_disarm, payload_arm_home, payload_arm_away, code):
|
||||
self._state = STATE_UNKNOWN
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
self._qos = qos
|
||||
self._payload_disarm = payload_disarm
|
||||
self._payload_arm_home = payload_arm_home
|
||||
self._payload_arm_away = payload_arm_away
|
||||
self._code = code
|
||||
|
||||
def message_received(topic, payload, qos):
|
||||
""" A new MQTT message has been received. """
|
||||
self._state = payload
|
||||
self.update_ha_state()
|
||||
|
||||
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" One or more characters if code is defined """
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
if code == str(self._code) or self.code_format is None:
|
||||
mqtt.publish(self.hass, self._command_topic,
|
||||
self._payload_disarm, self._qos)
|
||||
else:
|
||||
_LOGGER.warning("Wrong code entered while disarming!")
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
if code == str(self._code) or self.code_format is None:
|
||||
mqtt.publish(self.hass, self._command_topic,
|
||||
self._payload_arm_home, self._qos)
|
||||
else:
|
||||
_LOGGER.warning("Wrong code entered while arming home!")
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
if code == str(self._code) or self.code_format is None:
|
||||
mqtt.publish(self.hass, self._command_topic,
|
||||
self._payload_arm_away, self._qos)
|
||||
else:
|
||||
_LOGGER.warning("Wrong code entered while arming away!")
|
||||
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
homeassistant.components.alarm_control_panel.verisure
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Interfaces with Verisure alarm control panel.
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.components.verisure as verisure
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Verisure platform. """
|
||||
|
||||
if not verisure.MY_PAGES:
|
||||
_LOGGER.error('A connection has not been made to Verisure mypages.')
|
||||
return False
|
||||
|
||||
alarms = []
|
||||
|
||||
alarms.extend([
|
||||
VerisureAlarm(value)
|
||||
for value in verisure.get_alarm_status().values()
|
||||
if verisure.SHOW_ALARM
|
||||
])
|
||||
|
||||
add_devices(alarms)
|
||||
|
||||
|
||||
class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
""" Represents a Verisure alarm status. """
|
||||
|
||||
def __init__(self, alarm_status):
|
||||
self._id = alarm_status.id
|
||||
self._device = verisure.MY_PAGES.DEVICE_ALARM
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return 'Alarm {}'.format(self._id)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" Four digit code required. """
|
||||
return '^\\d{4}$'
|
||||
|
||||
def update(self):
|
||||
""" Update alarm status """
|
||||
verisure.update()
|
||||
|
||||
if verisure.STATUS[self._device][self._id].status == 'unarmed':
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif verisure.STATUS[self._device][self._id].status == 'armedhome':
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif verisure.STATUS[self._device][self._id].status == 'armedaway':
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
elif verisure.STATUS[self._device][self._id].status != 'pending':
|
||||
_LOGGER.error(
|
||||
'Unknown alarm state %s',
|
||||
verisure.STATUS[self._device][self._id].status)
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
verisure.MY_PAGES.set_alarm_status(
|
||||
code,
|
||||
verisure.MY_PAGES.ALARM_DISARMED)
|
||||
_LOGGER.warning('disarming')
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
verisure.MY_PAGES.set_alarm_status(
|
||||
code,
|
||||
verisure.MY_PAGES.ALARM_ARMED_HOME)
|
||||
_LOGGER.warning('arming home')
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
verisure.MY_PAGES.set_alarm_status(
|
||||
code,
|
||||
verisure.MY_PAGES.ALARM_ARMED_AWAY)
|
||||
_LOGGER.warning('arming away')
|
||||
@@ -103,6 +103,10 @@ def _handle_get_api_stream(handler, path_match, data):
|
||||
write_lock = threading.Lock()
|
||||
block = threading.Event()
|
||||
|
||||
restrict = data.get('restrict')
|
||||
if restrict:
|
||||
restrict = restrict.split(',')
|
||||
|
||||
def write_message(payload):
|
||||
""" Writes a message to the output. """
|
||||
with write_lock:
|
||||
@@ -118,7 +122,8 @@ def _handle_get_api_stream(handler, path_match, data):
|
||||
""" Forwards events to the open request. """
|
||||
nonlocal gracefully_closed
|
||||
|
||||
if block.is_set() or event.event_type == EVENT_TIME_CHANGED:
|
||||
if block.is_set() or event.event_type == EVENT_TIME_CHANGED or \
|
||||
restrict and event.event_type not in restrict:
|
||||
return
|
||||
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
|
||||
gracefully_closed = True
|
||||
|
||||
@@ -7,68 +7,217 @@ Allows to setup simple automation rules via the config file.
|
||||
import logging
|
||||
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.helpers import config_per_platform
|
||||
from homeassistant.util import split_entity_id
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
|
||||
from homeassistant.components import logbook
|
||||
|
||||
DOMAIN = "automation"
|
||||
DOMAIN = 'automation'
|
||||
|
||||
DEPENDENCIES = ["group"]
|
||||
DEPENDENCIES = ['group']
|
||||
|
||||
CONF_ALIAS = "alias"
|
||||
CONF_SERVICE = "execute_service"
|
||||
CONF_SERVICE_ENTITY_ID = "service_entity_id"
|
||||
CONF_SERVICE_DATA = "service_data"
|
||||
CONF_ALIAS = 'alias'
|
||||
CONF_SERVICE = 'service'
|
||||
CONF_SERVICE_ENTITY_ID = 'entity_id'
|
||||
CONF_SERVICE_DATA = 'data'
|
||||
|
||||
CONF_CONDITION = 'condition'
|
||||
CONF_ACTION = 'action'
|
||||
CONF_TRIGGER = 'trigger'
|
||||
CONF_CONDITION_TYPE = 'condition_type'
|
||||
|
||||
CONDITION_USE_TRIGGER_VALUES = 'use_trigger_values'
|
||||
CONDITION_TYPE_AND = 'and'
|
||||
CONDITION_TYPE_OR = 'or'
|
||||
|
||||
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Sets up automation. """
|
||||
success = False
|
||||
config_key = DOMAIN
|
||||
found = 1
|
||||
|
||||
for p_type, p_config in config_per_platform(config, DOMAIN, _LOGGER):
|
||||
platform = prepare_setup_platform(hass, config, DOMAIN, p_type)
|
||||
while config_key in config:
|
||||
# check for one block syntax
|
||||
if isinstance(config[config_key], dict):
|
||||
config_block = _migrate_old_config(config[config_key])
|
||||
name = config_block.get(CONF_ALIAS, config_key)
|
||||
_setup_automation(hass, config_block, name, config)
|
||||
|
||||
if platform is None:
|
||||
_LOGGER.error("Unknown automation platform specified: %s", p_type)
|
||||
continue
|
||||
# check for multiple block syntax
|
||||
elif isinstance(config[config_key], list):
|
||||
for list_no, config_block in enumerate(config[config_key]):
|
||||
name = config_block.get(CONF_ALIAS,
|
||||
"{}, {}".format(config_key, list_no))
|
||||
_setup_automation(hass, config_block, name, config)
|
||||
|
||||
if platform.register(hass, p_config, _get_action(hass, p_config)):
|
||||
_LOGGER.info(
|
||||
"Initialized %s rule %s", p_type, p_config.get(CONF_ALIAS, ""))
|
||||
success = True
|
||||
# any scalar value is incorrect
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Error setting up rule %s", p_config.get(CONF_ALIAS, ""))
|
||||
_LOGGER.error('Error in config in section %s.', config_key)
|
||||
|
||||
return success
|
||||
found += 1
|
||||
config_key = "{} {}".format(DOMAIN, found)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _get_action(hass, config):
|
||||
def _setup_automation(hass, config_block, name, config):
|
||||
""" Setup one instance of automation """
|
||||
|
||||
action = _get_action(hass, config_block.get(CONF_ACTION, {}), name)
|
||||
|
||||
if action is None:
|
||||
return False
|
||||
|
||||
if CONF_CONDITION in config_block or CONF_CONDITION_TYPE in config_block:
|
||||
action = _process_if(hass, config, config_block, action)
|
||||
|
||||
if action is None:
|
||||
return False
|
||||
|
||||
_process_trigger(hass, config, config_block.get(CONF_TRIGGER, []), name,
|
||||
action)
|
||||
return True
|
||||
|
||||
|
||||
def _get_action(hass, config, name):
|
||||
""" Return an action based on a config. """
|
||||
|
||||
if CONF_SERVICE not in config:
|
||||
_LOGGER.error('Error setting up %s, no action specified.', name)
|
||||
return None
|
||||
|
||||
def action():
|
||||
""" Action to be executed. """
|
||||
_LOGGER.info("Executing rule %s", config.get(CONF_ALIAS, ""))
|
||||
_LOGGER.info('Executing %s', name)
|
||||
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
|
||||
|
||||
if CONF_SERVICE in config:
|
||||
domain, service = split_entity_id(config[CONF_SERVICE])
|
||||
domain, service = split_entity_id(config[CONF_SERVICE])
|
||||
service_data = config.get(CONF_SERVICE_DATA, {})
|
||||
|
||||
service_data = config.get(CONF_SERVICE_DATA, {})
|
||||
if not isinstance(service_data, dict):
|
||||
_LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA)
|
||||
service_data = {}
|
||||
|
||||
if not isinstance(service_data, dict):
|
||||
_LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA)
|
||||
service_data = {}
|
||||
if CONF_SERVICE_ENTITY_ID in config:
|
||||
try:
|
||||
service_data[ATTR_ENTITY_ID] = \
|
||||
config[CONF_SERVICE_ENTITY_ID].split(",")
|
||||
except AttributeError:
|
||||
service_data[ATTR_ENTITY_ID] = \
|
||||
config[CONF_SERVICE_ENTITY_ID]
|
||||
|
||||
if CONF_SERVICE_ENTITY_ID in config:
|
||||
try:
|
||||
service_data[ATTR_ENTITY_ID] = \
|
||||
config[CONF_SERVICE_ENTITY_ID].split(",")
|
||||
except AttributeError:
|
||||
service_data[ATTR_ENTITY_ID] = \
|
||||
config[CONF_SERVICE_ENTITY_ID]
|
||||
|
||||
hass.services.call(domain, service, service_data)
|
||||
hass.services.call(domain, service, service_data)
|
||||
|
||||
return action
|
||||
|
||||
|
||||
def _migrate_old_config(config):
|
||||
""" Migrate old config to new. """
|
||||
if CONF_PLATFORM not in config:
|
||||
return config
|
||||
|
||||
_LOGGER.warning(
|
||||
'You are using an old configuration format. Please upgrade: '
|
||||
'https://home-assistant.io/components/automation.html')
|
||||
|
||||
new_conf = {
|
||||
CONF_TRIGGER: dict(config),
|
||||
CONF_CONDITION: config.get('if', []),
|
||||
CONF_ACTION: dict(config),
|
||||
}
|
||||
|
||||
for cat, key, new_key in (('trigger', 'mqtt_topic', 'topic'),
|
||||
('trigger', 'mqtt_payload', 'payload'),
|
||||
('trigger', 'state_entity_id', 'entity_id'),
|
||||
('trigger', 'state_before', 'before'),
|
||||
('trigger', 'state_after', 'after'),
|
||||
('trigger', 'state_to', 'to'),
|
||||
('trigger', 'state_from', 'from'),
|
||||
('trigger', 'state_hours', 'hours'),
|
||||
('trigger', 'state_minutes', 'minutes'),
|
||||
('trigger', 'state_seconds', 'seconds'),
|
||||
('action', 'execute_service', 'service'),
|
||||
('action', 'service_entity_id', 'entity_id'),
|
||||
('action', 'service_data', 'data')):
|
||||
if key in new_conf[cat]:
|
||||
new_conf[cat][new_key] = new_conf[cat].pop(key)
|
||||
|
||||
return new_conf
|
||||
|
||||
|
||||
def _process_if(hass, config, p_config, action):
|
||||
""" Processes if checks. """
|
||||
|
||||
cond_type = p_config.get(CONF_CONDITION_TYPE,
|
||||
DEFAULT_CONDITION_TYPE).lower()
|
||||
|
||||
if_configs = p_config.get(CONF_CONDITION)
|
||||
use_trigger = if_configs == CONDITION_USE_TRIGGER_VALUES
|
||||
|
||||
if use_trigger:
|
||||
if_configs = p_config[CONF_TRIGGER]
|
||||
|
||||
if isinstance(if_configs, dict):
|
||||
if_configs = [if_configs]
|
||||
|
||||
checks = []
|
||||
for if_config in if_configs:
|
||||
platform = _resolve_platform('if_action', hass, config,
|
||||
if_config.get(CONF_PLATFORM))
|
||||
if platform is None:
|
||||
continue
|
||||
|
||||
check = platform.if_action(hass, if_config)
|
||||
|
||||
# Invalid conditions are allowed if we base it on trigger
|
||||
if check is None and not use_trigger:
|
||||
return None
|
||||
|
||||
checks.append(check)
|
||||
|
||||
if cond_type == CONDITION_TYPE_AND:
|
||||
def if_action():
|
||||
""" AND all conditions. """
|
||||
if all(check() for check in checks):
|
||||
action()
|
||||
else:
|
||||
def if_action():
|
||||
""" OR all conditions. """
|
||||
if any(check() for check in checks):
|
||||
action()
|
||||
|
||||
return if_action
|
||||
|
||||
|
||||
def _process_trigger(hass, config, trigger_configs, name, action):
|
||||
""" Setup triggers. """
|
||||
if isinstance(trigger_configs, dict):
|
||||
trigger_configs = [trigger_configs]
|
||||
|
||||
for conf in trigger_configs:
|
||||
platform = _resolve_platform('trigger', hass, config,
|
||||
conf.get(CONF_PLATFORM))
|
||||
if platform is None:
|
||||
continue
|
||||
|
||||
if platform.trigger(hass, conf, action):
|
||||
_LOGGER.info("Initialized rule %s", name)
|
||||
else:
|
||||
_LOGGER.error("Error setting up rule %s", name)
|
||||
|
||||
|
||||
def _resolve_platform(method, hass, config, platform):
|
||||
""" Find automation platform. """
|
||||
if platform is None:
|
||||
return None
|
||||
platform = prepare_setup_platform(hass, config, DOMAIN, platform)
|
||||
|
||||
if platform is None or not hasattr(platform, method):
|
||||
_LOGGER.error("Unknown automation platform specified for %s: %s",
|
||||
method, platform)
|
||||
return None
|
||||
|
||||
return platform
|
||||
|
||||
@@ -12,7 +12,7 @@ CONF_EVENT_DATA = "event_data"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def register(hass, config, action):
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for events based on config. """
|
||||
event_type = config.get(CONF_EVENT_TYPE)
|
||||
|
||||
@@ -20,11 +20,12 @@ def register(hass, config, action):
|
||||
_LOGGER.error("Missing configuration key %s", CONF_EVENT_TYPE)
|
||||
return False
|
||||
|
||||
event_data = config.get(CONF_EVENT_DATA, {})
|
||||
event_data = config.get(CONF_EVENT_DATA)
|
||||
|
||||
def handle_event(event):
|
||||
""" Listens for events and calls the action when data matches. """
|
||||
if event_data == event.data:
|
||||
if not event_data or all(val == event.data.get(key) for key, val
|
||||
in event_data.items()):
|
||||
action()
|
||||
|
||||
hass.bus.listen(event_type, handle_event)
|
||||
|
||||
@@ -10,11 +10,11 @@ import homeassistant.components.mqtt as mqtt
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
CONF_TOPIC = 'mqtt_topic'
|
||||
CONF_PAYLOAD = 'mqtt_payload'
|
||||
CONF_TOPIC = 'topic'
|
||||
CONF_PAYLOAD = 'payload'
|
||||
|
||||
|
||||
def register(hass, config, action):
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
topic = config.get(CONF_TOPIC)
|
||||
payload = config.get(CONF_PAYLOAD)
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
homeassistant.components.automation.numeric_state
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Offers numeric state listening automation rules.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
|
||||
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_BELOW = "below"
|
||||
CONF_ABOVE = "above"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
|
||||
if entity_id is None:
|
||||
_LOGGER.error("Missing configuration key %s", CONF_ENTITY_ID)
|
||||
return False
|
||||
|
||||
below = config.get(CONF_BELOW)
|
||||
above = config.get(CONF_ABOVE)
|
||||
|
||||
if below is None and above is None:
|
||||
_LOGGER.error("Missing configuration key."
|
||||
" One of %s or %s is required",
|
||||
CONF_BELOW, CONF_ABOVE)
|
||||
return False
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
""" Listens for state changes and calls action. """
|
||||
|
||||
# Fire action if we go from outside range into range
|
||||
if _in_range(to_s.state, above, below) and \
|
||||
(from_s is None or not _in_range(from_s.state, above, below)):
|
||||
action()
|
||||
|
||||
track_state_change(
|
||||
hass, entity_id, state_automation_listener)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
""" Wraps action method with state based condition. """
|
||||
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
|
||||
if entity_id is None:
|
||||
_LOGGER.error("Missing configuration key %s", CONF_ENTITY_ID)
|
||||
return None
|
||||
|
||||
below = config.get(CONF_BELOW)
|
||||
above = config.get(CONF_ABOVE)
|
||||
|
||||
if below is None and above is None:
|
||||
_LOGGER.error("Missing configuration key."
|
||||
" One of %s or %s is required",
|
||||
CONF_BELOW, CONF_ABOVE)
|
||||
return None
|
||||
|
||||
def if_numeric_state():
|
||||
""" Test numeric state condition. """
|
||||
state = hass.states.get(entity_id)
|
||||
return state is not None and _in_range(state.state, above, below)
|
||||
|
||||
return if_numeric_state
|
||||
|
||||
|
||||
def _in_range(value, range_start, range_end):
|
||||
""" Checks if value is inside the range """
|
||||
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
_LOGGER.warn("Missing value in numeric check")
|
||||
return False
|
||||
|
||||
if range_start is not None and range_end is not None:
|
||||
return float(range_start) <= value < float(range_end)
|
||||
elif range_end is not None:
|
||||
return value < float(range_end)
|
||||
else:
|
||||
return float(range_start) <= value
|
||||
@@ -10,22 +10,23 @@ from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.const import MATCH_ALL
|
||||
|
||||
|
||||
CONF_ENTITY_ID = "state_entity_id"
|
||||
CONF_FROM = "state_from"
|
||||
CONF_TO = "state_to"
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_FROM = "from"
|
||||
CONF_TO = "to"
|
||||
CONF_STATE = "state"
|
||||
|
||||
|
||||
def register(hass, config, action):
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
|
||||
if entity_id is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing configuration key %s", CONF_ENTITY_ID)
|
||||
"Missing trigger configuration key %s", CONF_ENTITY_ID)
|
||||
return False
|
||||
|
||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||
to_state = config.get(CONF_TO, MATCH_ALL)
|
||||
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
|
||||
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
""" Listens for state changes and calls action. """
|
||||
@@ -35,3 +36,23 @@ def register(hass, config, action):
|
||||
hass, entity_id, state_automation_listener, from_state, to_state)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
""" Wraps action method with state based condition. """
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
state = config.get(CONF_STATE)
|
||||
|
||||
if entity_id is None or state is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing if-condition configuration key %s or %s", CONF_ENTITY_ID,
|
||||
CONF_STATE)
|
||||
return None
|
||||
|
||||
state = str(state)
|
||||
|
||||
def if_state():
|
||||
""" Test if condition. """
|
||||
return hass.states.is_state(entity_id, state)
|
||||
|
||||
return if_state
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
homeassistant.components.automation.sun
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Offers sun based automation rules.
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components import sun
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
DEPENDENCIES = ['sun']
|
||||
|
||||
CONF_OFFSET = 'offset'
|
||||
CONF_EVENT = 'event'
|
||||
|
||||
EVENT_SUNSET = 'sunset'
|
||||
EVENT_SUNRISE = 'sunrise'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for events based on config. """
|
||||
event = config.get(CONF_EVENT)
|
||||
|
||||
if event is None:
|
||||
_LOGGER.error("Missing configuration key %s", CONF_EVENT)
|
||||
return False
|
||||
|
||||
event = event.lower()
|
||||
if event not in (EVENT_SUNRISE, EVENT_SUNSET):
|
||||
_LOGGER.error("Invalid value for %s: %s", CONF_EVENT, event)
|
||||
return False
|
||||
|
||||
if CONF_OFFSET in config:
|
||||
raw_offset = config.get(CONF_OFFSET)
|
||||
|
||||
negative_offset = False
|
||||
if raw_offset.startswith('-'):
|
||||
negative_offset = True
|
||||
raw_offset = raw_offset[1:]
|
||||
|
||||
try:
|
||||
(hour, minute, second) = [int(x) for x in raw_offset.split(':')]
|
||||
except ValueError:
|
||||
_LOGGER.error('Could not parse offset %s', raw_offset)
|
||||
return False
|
||||
|
||||
offset = timedelta(hours=hour, minutes=minute, seconds=second)
|
||||
|
||||
if negative_offset:
|
||||
offset *= -1
|
||||
else:
|
||||
offset = timedelta(0)
|
||||
|
||||
# Do something to call action
|
||||
if event == EVENT_SUNRISE:
|
||||
trigger_sunrise(hass, action, offset)
|
||||
else:
|
||||
trigger_sunset(hass, action, offset)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def trigger_sunrise(hass, action, offset):
|
||||
""" Trigger action at next sun rise. """
|
||||
def next_rise():
|
||||
""" Returns next sunrise. """
|
||||
next_time = sun.next_rising_utc(hass) + offset
|
||||
|
||||
while next_time < dt_util.utcnow():
|
||||
next_time = next_time + timedelta(days=1)
|
||||
|
||||
return next_time
|
||||
|
||||
def sunrise_automation_listener(now):
|
||||
""" Called when it's time for action. """
|
||||
track_point_in_utc_time(hass, sunrise_automation_listener, next_rise())
|
||||
action()
|
||||
|
||||
track_point_in_utc_time(hass, sunrise_automation_listener, next_rise())
|
||||
|
||||
|
||||
def trigger_sunset(hass, action, offset):
|
||||
""" Trigger action at next sun set. """
|
||||
def next_set():
|
||||
""" Returns next sunrise. """
|
||||
next_time = sun.next_setting_utc(hass) + offset
|
||||
|
||||
while next_time < dt_util.utcnow():
|
||||
next_time = next_time + timedelta(days=1)
|
||||
|
||||
return next_time
|
||||
|
||||
def sunset_automation_listener(now):
|
||||
""" Called when it's time for action. """
|
||||
track_point_in_utc_time(hass, sunset_automation_listener, next_set())
|
||||
action()
|
||||
|
||||
track_point_in_utc_time(hass, sunset_automation_listener, next_set())
|
||||
@@ -4,19 +4,41 @@ homeassistant.components.automation.time
|
||||
|
||||
Offers time listening automation rules.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.util import convert
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import track_time_change
|
||||
|
||||
CONF_HOURS = "time_hours"
|
||||
CONF_MINUTES = "time_minutes"
|
||||
CONF_SECONDS = "time_seconds"
|
||||
CONF_HOURS = "hours"
|
||||
CONF_MINUTES = "minutes"
|
||||
CONF_SECONDS = "seconds"
|
||||
CONF_BEFORE = "before"
|
||||
CONF_AFTER = "after"
|
||||
CONF_WEEKDAY = "weekday"
|
||||
|
||||
WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def register(hass, config, action):
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
hours = convert(config.get(CONF_HOURS), int)
|
||||
minutes = convert(config.get(CONF_MINUTES), int)
|
||||
seconds = convert(config.get(CONF_SECONDS), int)
|
||||
if CONF_AFTER in config:
|
||||
after = dt_util.parse_time_str(config[CONF_AFTER])
|
||||
if after is None:
|
||||
_error_time(config[CONF_AFTER], CONF_AFTER)
|
||||
return False
|
||||
hours, minutes, seconds = after.hour, after.minute, after.second
|
||||
elif (CONF_HOURS in config or CONF_MINUTES in config
|
||||
or CONF_SECONDS in config):
|
||||
hours = convert(config.get(CONF_HOURS), int)
|
||||
minutes = convert(config.get(CONF_MINUTES), int)
|
||||
seconds = convert(config.get(CONF_SECONDS), int)
|
||||
else:
|
||||
_LOGGER.error('One of %s, %s, %s OR %s needs to be specified',
|
||||
CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AFTER)
|
||||
return False
|
||||
|
||||
def time_automation_listener(now):
|
||||
""" Listens for time changes and calls action. """
|
||||
@@ -26,3 +48,58 @@ def register(hass, config, action):
|
||||
hour=hours, minute=minutes, second=seconds)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
""" Wraps action method with time based condition. """
|
||||
before = config.get(CONF_BEFORE)
|
||||
after = config.get(CONF_AFTER)
|
||||
weekday = config.get(CONF_WEEKDAY)
|
||||
|
||||
if before is None and after is None and weekday is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing if-condition configuration key %s, %s or %s",
|
||||
CONF_BEFORE, CONF_AFTER, CONF_WEEKDAY)
|
||||
return None
|
||||
|
||||
if before is not None:
|
||||
before = dt_util.parse_time_str(before)
|
||||
if before is None:
|
||||
_error_time(before, CONF_BEFORE)
|
||||
return None
|
||||
|
||||
if after is not None:
|
||||
after = dt_util.parse_time_str(after)
|
||||
if after is None:
|
||||
_error_time(after, CONF_AFTER)
|
||||
return None
|
||||
|
||||
def time_if():
|
||||
""" Validate time based if-condition """
|
||||
now = dt_util.now()
|
||||
if before is not None and now > now.replace(hour=before.hour,
|
||||
minute=before.minute):
|
||||
return False
|
||||
|
||||
if after is not None and now < now.replace(hour=after.hour,
|
||||
minute=after.minute):
|
||||
return False
|
||||
|
||||
if weekday is not None:
|
||||
now_weekday = WEEKDAYS[now.weekday()]
|
||||
|
||||
if isinstance(weekday, str) and weekday != now_weekday or \
|
||||
now_weekday not in weekday:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return time_if
|
||||
|
||||
|
||||
def _error_time(value, key):
|
||||
""" Helper method to print error. """
|
||||
_LOGGER.error(
|
||||
"Received invalid value for '%s': %s", key, value)
|
||||
if isinstance(value, int):
|
||||
_LOGGER.error('Make sure you wrap time values in quotes')
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
homeassistant.components.automation.zone
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Offers zone automation rules.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components import zone
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.const import (
|
||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL)
|
||||
|
||||
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_ZONE = "zone"
|
||||
CONF_EVENT = "event"
|
||||
EVENT_ENTER = "enter"
|
||||
EVENT_LEAVE = "leave"
|
||||
DEFAULT_EVENT = EVENT_ENTER
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
|
||||
if entity_id is None or zone_entity_id is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing trigger configuration key %s or %s", CONF_ENTITY_ID,
|
||||
CONF_ZONE)
|
||||
return False
|
||||
|
||||
event = config.get(CONF_EVENT, DEFAULT_EVENT)
|
||||
|
||||
def zone_automation_listener(entity, from_s, to_s):
|
||||
""" Listens for state changes and calls action. """
|
||||
if from_s and None in (from_s.attributes.get(ATTR_LATITUDE),
|
||||
from_s.attributes.get(ATTR_LONGITUDE)) or \
|
||||
None in (to_s.attributes.get(ATTR_LATITUDE),
|
||||
to_s.attributes.get(ATTR_LONGITUDE)):
|
||||
return
|
||||
|
||||
from_match = _in_zone(hass, zone_entity_id, from_s) if from_s else None
|
||||
to_match = _in_zone(hass, zone_entity_id, to_s)
|
||||
|
||||
if event == EVENT_ENTER and not from_match and to_match or \
|
||||
event == EVENT_LEAVE and from_match and not to_match:
|
||||
action()
|
||||
|
||||
track_state_change(
|
||||
hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
""" Wraps action method with zone based condition. """
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
|
||||
if entity_id is None or zone_entity_id is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing condition configuration key %s or %s", CONF_ENTITY_ID,
|
||||
CONF_ZONE)
|
||||
return False
|
||||
|
||||
def if_in_zone():
|
||||
""" Test if condition. """
|
||||
return _in_zone(hass, zone_entity_id, hass.states.get(entity_id))
|
||||
|
||||
return if_in_zone
|
||||
|
||||
|
||||
def _in_zone(hass, zone_entity_id, state):
|
||||
""" Check if state is in zone. """
|
||||
if not state or None in (state.attributes.get(ATTR_LATITUDE),
|
||||
state.attributes.get(ATTR_LONGITUDE)):
|
||||
return False
|
||||
|
||||
zone_state = hass.states.get(zone_entity_id)
|
||||
return zone_state and zone.in_zone(
|
||||
zone_state, state.attributes.get(ATTR_LATITUDE),
|
||||
state.attributes.get(ATTR_LONGITUDE),
|
||||
state.attributes.get(ATTR_GPS_ACCURACY, 0))
|
||||
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
homeassistant.components.camera.foscam
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
This component provides basic support for Foscam IP cameras.
|
||||
|
||||
As part of the basic support the following features will be provided:
|
||||
-MJPEG video streaming
|
||||
|
||||
To use this component, add the following to your configuration.yaml file.
|
||||
|
||||
camera:
|
||||
platform: foscam
|
||||
name: Door Camera
|
||||
ip: 192.168.0.123
|
||||
port: 88
|
||||
username: YOUR_USERNAME
|
||||
password: YOUR_PASSWORD
|
||||
|
||||
Variables:
|
||||
|
||||
ip
|
||||
*Required
|
||||
The IP address of your Foscam device.
|
||||
|
||||
username
|
||||
*Required
|
||||
The username of a visitor or operator of your camera. Oddly admin accounts
|
||||
don't seem to have access to take snapshots.
|
||||
|
||||
password
|
||||
*Required
|
||||
The password for accessing your camera.
|
||||
|
||||
name
|
||||
*Optional
|
||||
This parameter allows you to override the name of your camera in homeassistant.
|
||||
|
||||
port
|
||||
*Optional
|
||||
The port that the camera is running on. The default is 88.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.foscam.html
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.camera import DOMAIN
|
||||
from homeassistant.components.camera import Camera
|
||||
import requests
|
||||
import re
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Adds a Foscam IP Camera. """
|
||||
if not validate_config({DOMAIN: config},
|
||||
{DOMAIN: ['username', 'password', 'ip']}, _LOGGER):
|
||||
return None
|
||||
|
||||
add_devices_callback([FoscamCamera(config)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class FoscamCamera(Camera):
|
||||
""" An implementation of a Foscam IP camera. """
|
||||
|
||||
def __init__(self, device_info):
|
||||
super(FoscamCamera, self).__init__()
|
||||
|
||||
ip_address = device_info.get('ip')
|
||||
port = device_info.get('port', 88)
|
||||
|
||||
self._base_url = 'http://' + ip_address + ':' + str(port) + '/'
|
||||
self._username = device_info.get('username')
|
||||
self._password = device_info.get('password')
|
||||
self._snap_picture_url = self._base_url \
|
||||
+ 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture&usr=' \
|
||||
+ self._username + '&pwd=' + self._password
|
||||
self._name = device_info.get('name', 'Foscam Camera')
|
||||
|
||||
_LOGGER.info('Using the following URL for %s: %s',
|
||||
self._name, self._snap_picture_url)
|
||||
|
||||
def camera_image(self):
|
||||
""" Return a still image reponse from the camera. """
|
||||
|
||||
# send the request to snap a picture
|
||||
response = requests.get(self._snap_picture_url)
|
||||
|
||||
# parse the response to find the image file name
|
||||
|
||||
pattern = re.compile('src="[.][.]/(.*[.]jpg)"')
|
||||
filename = pattern.search(response.content.decode("utf-8")).group(1)
|
||||
|
||||
# send request for the image
|
||||
response = requests.get(self._base_url + filename)
|
||||
|
||||
return response.content
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Return the name of this device. """
|
||||
return self._name
|
||||
@@ -17,7 +17,7 @@ DOMAIN = "demo"
|
||||
DEPENDENCIES = ['introduction', 'conversation']
|
||||
|
||||
COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||
'switch', 'light', 'thermostat', 'sensor', 'media_player', 'notify']
|
||||
'switch', 'light', 'sensor', 'thermostat', 'media_player', 'notify']
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@@ -33,10 +33,10 @@ def setup(hass, config):
|
||||
|
||||
# Setup sun
|
||||
if not hass.config.latitude:
|
||||
hass.config.latitude = '32.87336'
|
||||
hass.config.latitude = 32.87336
|
||||
|
||||
if not hass.config.longitude:
|
||||
hass.config.longitude = '117.22743'
|
||||
hass.config.longitude = 117.22743
|
||||
|
||||
bootstrap.setup_component(hass, 'sun')
|
||||
|
||||
@@ -60,7 +60,7 @@ def setup(hass, config):
|
||||
{'camera': {
|
||||
'platform': 'generic',
|
||||
'name': 'IP Camera',
|
||||
'still_image_url': 'http://194.218.96.92/jpg/image.jpg',
|
||||
'still_image_url': 'http://home-assistant.io/demo/webcam.jpg',
|
||||
}})
|
||||
|
||||
# Setup scripts
|
||||
@@ -108,7 +108,9 @@ def setup(hass, config):
|
||||
"http://graph.facebook.com/297400035/picture",
|
||||
ATTR_FRIENDLY_NAME: 'Paulus'})
|
||||
hass.states.set("device_tracker.anne_therese", "not_home",
|
||||
{ATTR_FRIENDLY_NAME: 'Anne Therese'})
|
||||
{ATTR_FRIENDLY_NAME: 'Anne Therese',
|
||||
'latitude': hass.config.latitude + 0.002,
|
||||
'longitude': hass.config.longitude + 0.002})
|
||||
|
||||
hass.states.set("group.all_devices", "home",
|
||||
{
|
||||
|
||||
@@ -1,52 +1,90 @@
|
||||
"""
|
||||
homeassistant.components.tracker
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
homeassistant.components.device_tracker
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides functionality to keep track of devices.
|
||||
|
||||
device_tracker:
|
||||
platform: netgear
|
||||
|
||||
# Optional
|
||||
|
||||
# How many seconds to wait after not seeing device to consider it not home
|
||||
consider_home: 180
|
||||
|
||||
# Seconds between each scan
|
||||
interval_seconds: 12
|
||||
|
||||
# New found devices auto found
|
||||
track_new_devices: yes
|
||||
|
||||
# Maximum distance from home we consider people home
|
||||
range_home: 100
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
import os
|
||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||
# pylint: disable=too-many-locals
|
||||
import csv
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.helpers.entity import _OVERWRITE
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.components import discovery, group, zone
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_per_platform
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.const import (
|
||||
STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME,
|
||||
CONF_PLATFORM, DEVICE_DEFAULT_NAME)
|
||||
from homeassistant.components import group
|
||||
ATTR_ENTITY_PICTURE, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
|
||||
|
||||
DOMAIN = "device_tracker"
|
||||
DEPENDENCIES = []
|
||||
|
||||
SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv"
|
||||
DEPENDENCIES = ['zone']
|
||||
|
||||
GROUP_NAME_ALL_DEVICES = 'all devices'
|
||||
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices')
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
# After how much time do we consider a device not home if
|
||||
# it does not show up on scans
|
||||
TIME_DEVICE_NOT_FOUND = timedelta(minutes=3)
|
||||
CSV_DEVICES = "known_devices.csv"
|
||||
YAML_DEVICES = 'known_devices.yaml'
|
||||
|
||||
# Filename to save known devices to
|
||||
KNOWN_DEVICES_FILE = "known_devices.csv"
|
||||
CONF_TRACK_NEW = "track_new_devices"
|
||||
DEFAULT_CONF_TRACK_NEW = True
|
||||
|
||||
CONF_SECONDS = "interval_seconds"
|
||||
CONF_CONSIDER_HOME = 'consider_home'
|
||||
DEFAULT_CONSIDER_HOME = 180 # seconds
|
||||
|
||||
DEFAULT_CONF_SECONDS = 12
|
||||
CONF_SCAN_INTERVAL = "interval_seconds"
|
||||
DEFAULT_SCAN_INTERVAL = 12
|
||||
|
||||
TRACK_NEW_DEVICES = "track_new_devices"
|
||||
CONF_AWAY_HIDE = 'hide_if_away'
|
||||
DEFAULT_AWAY_HIDE = False
|
||||
|
||||
CONF_HOME_RANGE = 'home_range'
|
||||
DEFAULT_HOME_RANGE = 100
|
||||
|
||||
SERVICE_SEE = 'see'
|
||||
|
||||
ATTR_MAC = 'mac'
|
||||
ATTR_DEV_ID = 'dev_id'
|
||||
ATTR_HOST_NAME = 'host_name'
|
||||
ATTR_LOCATION_NAME = 'location_name'
|
||||
ATTR_GPS = 'gps'
|
||||
ATTR_BATTERY = 'battery'
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
discovery.SERVICE_NETGEAR: 'netgear',
|
||||
}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
""" Returns if any or specified device is home. """
|
||||
@@ -55,293 +93,354 @@ def is_on(hass, entity_id=None):
|
||||
return hass.states.is_state(entity, STATE_HOME)
|
||||
|
||||
|
||||
def see(hass, mac=None, dev_id=None, host_name=None, location_name=None,
|
||||
gps=None, gps_accuracy=None, battery=None):
|
||||
""" Call service to notify you see device. """
|
||||
data = {key: value for key, value in
|
||||
((ATTR_MAC, mac),
|
||||
(ATTR_DEV_ID, dev_id),
|
||||
(ATTR_HOST_NAME, host_name),
|
||||
(ATTR_LOCATION_NAME, location_name),
|
||||
(ATTR_GPS, gps)) if value is not None}
|
||||
hass.services.call(DOMAIN, SERVICE_SEE, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Sets up the device tracker. """
|
||||
""" Setup device tracker """
|
||||
yaml_path = hass.config.path(YAML_DEVICES)
|
||||
csv_path = hass.config.path(CSV_DEVICES)
|
||||
if os.path.isfile(csv_path) and not os.path.isfile(yaml_path) and \
|
||||
convert_csv_config(csv_path, yaml_path):
|
||||
os.remove(csv_path)
|
||||
|
||||
if not validate_config(config, {DOMAIN: [CONF_PLATFORM]}, _LOGGER):
|
||||
return False
|
||||
conf = config.get(DOMAIN, {})
|
||||
consider_home = timedelta(
|
||||
seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int,
|
||||
DEFAULT_CONSIDER_HOME))
|
||||
track_new = util.convert(conf.get(CONF_TRACK_NEW), bool,
|
||||
DEFAULT_CONF_TRACK_NEW)
|
||||
home_range = util.convert(conf.get(CONF_HOME_RANGE), int,
|
||||
DEFAULT_HOME_RANGE)
|
||||
|
||||
tracker_type = config[DOMAIN].get(CONF_PLATFORM)
|
||||
devices = load_config(yaml_path, hass, consider_home, home_range)
|
||||
tracker = DeviceTracker(hass, consider_home, track_new, home_range,
|
||||
devices)
|
||||
|
||||
tracker_implementation = \
|
||||
prepare_setup_platform(hass, config, DOMAIN, tracker_type)
|
||||
|
||||
if tracker_implementation is None:
|
||||
_LOGGER.error("Unknown device_tracker type specified: %s.",
|
||||
tracker_type)
|
||||
|
||||
return False
|
||||
|
||||
device_scanner = tracker_implementation.get_scanner(hass, config)
|
||||
|
||||
if device_scanner is None:
|
||||
_LOGGER.error("Failed to initialize device scanner: %s",
|
||||
tracker_type)
|
||||
|
||||
return False
|
||||
|
||||
seconds = util.convert(config[DOMAIN].get(CONF_SECONDS), int,
|
||||
DEFAULT_CONF_SECONDS)
|
||||
|
||||
track_new_devices = config[DOMAIN].get(TRACK_NEW_DEVICES) or False
|
||||
_LOGGER.info("Tracking new devices: %s", track_new_devices)
|
||||
|
||||
tracker = DeviceTracker(hass, device_scanner, seconds, track_new_devices)
|
||||
|
||||
# We only succeeded if we got to parse the known devices file
|
||||
return not tracker.invalid_known_devices_file
|
||||
|
||||
|
||||
class DeviceTracker(object):
|
||||
""" Class that tracks which devices are home and which are not. """
|
||||
|
||||
def __init__(self, hass, device_scanner, seconds, track_new_devices):
|
||||
self.hass = hass
|
||||
|
||||
self.device_scanner = device_scanner
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
# Do we track new devices by default?
|
||||
self.track_new_devices = track_new_devices
|
||||
|
||||
# Dictionary to keep track of known devices and devices we track
|
||||
self.tracked = {}
|
||||
self.untracked_devices = set()
|
||||
|
||||
# Did we encounter an invalid known devices file
|
||||
self.invalid_known_devices_file = False
|
||||
|
||||
# Wrap it in a func instead of lambda so it can be identified in
|
||||
# the bus by its __name__ attribute.
|
||||
def update_device_state(now):
|
||||
""" Triggers update of the device states. """
|
||||
self.update_devices(now)
|
||||
|
||||
dev_group = group.Group(
|
||||
hass, GROUP_NAME_ALL_DEVICES, user_defined=False)
|
||||
|
||||
def reload_known_devices_service(service):
|
||||
""" Reload known devices file. """
|
||||
self._read_known_devices_file()
|
||||
|
||||
self.update_devices(dt_util.utcnow())
|
||||
|
||||
dev_group.update_tracked_entity_ids(self.device_entity_ids)
|
||||
|
||||
reload_known_devices_service(None)
|
||||
|
||||
if self.invalid_known_devices_file:
|
||||
return
|
||||
|
||||
seconds = range(0, 60, seconds)
|
||||
|
||||
_LOGGER.info("Device tracker interval second=%s", seconds)
|
||||
track_utc_time_change(hass, update_device_state, second=seconds)
|
||||
|
||||
hass.services.register(DOMAIN,
|
||||
SERVICE_DEVICE_TRACKER_RELOAD,
|
||||
reload_known_devices_service)
|
||||
|
||||
@property
|
||||
def device_entity_ids(self):
|
||||
""" Returns a set containing all device entity ids
|
||||
that are being tracked. """
|
||||
return set(device['entity_id'] for device in self.tracked.values())
|
||||
|
||||
def _update_state(self, now, device, is_home):
|
||||
""" Update the state of a device. """
|
||||
dev_info = self.tracked[device]
|
||||
|
||||
if is_home:
|
||||
# Update last seen if at home
|
||||
dev_info['last_seen'] = now
|
||||
else:
|
||||
# State remains at home if it has been seen in the last
|
||||
# TIME_DEVICE_NOT_FOUND
|
||||
is_home = now - dev_info['last_seen'] < TIME_DEVICE_NOT_FOUND
|
||||
|
||||
state = STATE_HOME if is_home else STATE_NOT_HOME
|
||||
|
||||
# overwrite properties that have been set in the config file
|
||||
attr = dict(dev_info['state_attr'])
|
||||
attr.update(_OVERWRITE.get(dev_info['entity_id'], {}))
|
||||
|
||||
self.hass.states.set(
|
||||
dev_info['entity_id'], state, attr)
|
||||
|
||||
def update_devices(self, now):
|
||||
""" Update device states based on the found devices. """
|
||||
if not self.lock.acquire(False):
|
||||
def setup_platform(p_type, p_config, disc_info=None):
|
||||
""" Setup a device tracker platform. """
|
||||
platform = prepare_setup_platform(hass, config, DOMAIN, p_type)
|
||||
if platform is None:
|
||||
return
|
||||
|
||||
try:
|
||||
found_devices = set(dev.upper() for dev in
|
||||
self.device_scanner.scan_devices())
|
||||
if hasattr(platform, 'get_scanner'):
|
||||
scanner = platform.get_scanner(hass, {DOMAIN: p_config})
|
||||
|
||||
for device in self.tracked:
|
||||
is_home = device in found_devices
|
||||
if scanner is None:
|
||||
_LOGGER.error('Error setting up platform %s', p_type)
|
||||
return
|
||||
|
||||
self._update_state(now, device, is_home)
|
||||
setup_scanner_platform(hass, p_config, scanner, tracker.see)
|
||||
return
|
||||
|
||||
if is_home:
|
||||
found_devices.remove(device)
|
||||
if not platform.setup_scanner(hass, p_config, tracker.see):
|
||||
_LOGGER.error('Error setting up platform %s', p_type)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception('Error setting up platform %s', p_type)
|
||||
|
||||
# Did we find any devices that we didn't know about yet?
|
||||
new_devices = found_devices - self.untracked_devices
|
||||
for p_type, p_config in \
|
||||
config_per_platform(config, DOMAIN, _LOGGER):
|
||||
setup_platform(p_type, p_config)
|
||||
|
||||
if new_devices:
|
||||
if not self.track_new_devices:
|
||||
self.untracked_devices.update(new_devices)
|
||||
def device_tracker_discovered(service, info):
|
||||
""" Called when a device tracker platform is discovered. """
|
||||
setup_platform(DISCOVERY_PLATFORMS[service], {}, info)
|
||||
|
||||
self._update_known_devices_file(new_devices)
|
||||
finally:
|
||||
self.lock.release()
|
||||
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(),
|
||||
device_tracker_discovered)
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def _read_known_devices_file(self):
|
||||
""" Parse and process the known devices file. """
|
||||
known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE)
|
||||
def update_stale(now):
|
||||
""" Clean up stale devices. """
|
||||
tracker.update_stale(now)
|
||||
track_utc_time_change(hass, update_stale, second=range(0, 60, 5))
|
||||
|
||||
# Return if no known devices file exists
|
||||
if not os.path.isfile(known_dev_path):
|
||||
tracker.setup_group()
|
||||
|
||||
def see_service(call):
|
||||
""" Service to see a device. """
|
||||
args = {key: value for key, value in call.data.items() if key in
|
||||
(ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME,
|
||||
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY)}
|
||||
tracker.see(**args)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
hass.services.register(DOMAIN, SERVICE_SEE, see_service,
|
||||
descriptions.get(SERVICE_SEE))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class DeviceTracker(object):
|
||||
""" Track devices """
|
||||
def __init__(self, hass, consider_home, track_new, home_range, devices):
|
||||
self.hass = hass
|
||||
self.devices = {dev.dev_id: dev for dev in devices}
|
||||
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
|
||||
self.consider_home = consider_home
|
||||
self.track_new = track_new
|
||||
self.home_range = home_range
|
||||
self.lock = threading.Lock()
|
||||
|
||||
for device in devices:
|
||||
if device.track:
|
||||
device.update_ha_state()
|
||||
|
||||
self.group = None
|
||||
|
||||
def see(self, mac=None, dev_id=None, host_name=None, location_name=None,
|
||||
gps=None, gps_accuracy=None, battery=None):
|
||||
""" Notify device tracker that you see a device. """
|
||||
with self.lock:
|
||||
if mac is None and dev_id is None:
|
||||
raise HomeAssistantError('Neither mac or device id passed in')
|
||||
elif mac is not None:
|
||||
mac = mac.upper()
|
||||
device = self.mac_to_dev.get(mac)
|
||||
if not device:
|
||||
dev_id = util.slugify(host_name or '') or util.slugify(mac)
|
||||
else:
|
||||
dev_id = str(dev_id).lower()
|
||||
device = self.devices.get(dev_id)
|
||||
|
||||
if device:
|
||||
device.seen(host_name, location_name, gps, gps_accuracy,
|
||||
battery)
|
||||
if device.track:
|
||||
device.update_ha_state()
|
||||
return
|
||||
|
||||
# If no device can be found, create it
|
||||
device = Device(
|
||||
self.hass, self.consider_home, self.home_range, self.track_new,
|
||||
dev_id, mac, (host_name or dev_id).replace('_', ' '))
|
||||
self.devices[dev_id] = device
|
||||
if mac is not None:
|
||||
self.mac_to_dev[mac] = device
|
||||
|
||||
device.seen(host_name, location_name, gps, gps_accuracy, battery)
|
||||
if device.track:
|
||||
device.update_ha_state()
|
||||
|
||||
# During init, we ignore the group
|
||||
if self.group is not None:
|
||||
self.group.update_tracked_entity_ids(
|
||||
list(self.group.tracking) + [device.entity_id])
|
||||
update_config(self.hass.config.path(YAML_DEVICES), dev_id, device)
|
||||
|
||||
def setup_group(self):
|
||||
""" Initializes group for all tracked devices. """
|
||||
entity_ids = (dev.entity_id for dev in self.devices.values()
|
||||
if dev.track)
|
||||
self.group = group.setup_group(
|
||||
self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False)
|
||||
|
||||
def update_stale(self, now):
|
||||
""" Update stale devices. """
|
||||
with self.lock:
|
||||
for device in self.devices.values():
|
||||
if (device.track and device.last_update_home and
|
||||
device.stale(now)):
|
||||
device.update_ha_state(True)
|
||||
|
||||
|
||||
class Device(Entity):
|
||||
""" Tracked device. """
|
||||
|
||||
host_name = None
|
||||
location_name = None
|
||||
gps = None
|
||||
gps_accuracy = 0
|
||||
last_seen = None
|
||||
battery = None
|
||||
|
||||
# Track if the last update of this device was HOME
|
||||
last_update_home = False
|
||||
_state = STATE_NOT_HOME
|
||||
|
||||
def __init__(self, hass, consider_home, home_range, track, dev_id, mac,
|
||||
name=None, picture=None, away_hide=False):
|
||||
self.hass = hass
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
||||
|
||||
# Timedelta object how long we consider a device home if it is not
|
||||
# detected anymore.
|
||||
self.consider_home = consider_home
|
||||
|
||||
# Distance in meters
|
||||
self.home_range = home_range
|
||||
# Device ID
|
||||
self.dev_id = dev_id
|
||||
self.mac = mac
|
||||
|
||||
# If we should track this device
|
||||
self.track = track
|
||||
|
||||
# Configured name
|
||||
self.config_name = name
|
||||
|
||||
# Configured picture
|
||||
self.config_picture = picture
|
||||
self.away_hide = away_hide
|
||||
|
||||
@property
|
||||
def gps_home(self):
|
||||
""" Return if device is within range of home. """
|
||||
distance = max(
|
||||
0, self.hass.config.distance(*self.gps) - self.gps_accuracy)
|
||||
return self.gps is not None and distance <= self.home_range
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the entity. """
|
||||
return self.config_name or self.host_name or DEVICE_DEFAULT_NAME
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" State of the device. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Device state attributes. """
|
||||
attr = {}
|
||||
|
||||
if self.config_picture:
|
||||
attr[ATTR_ENTITY_PICTURE] = self.config_picture
|
||||
|
||||
if self.gps:
|
||||
attr[ATTR_LATITUDE] = self.gps[0]
|
||||
attr[ATTR_LONGITUDE] = self.gps[1]
|
||||
attr[ATTR_GPS_ACCURACY] = self.gps_accuracy
|
||||
|
||||
if self.battery:
|
||||
attr[ATTR_BATTERY] = self.battery
|
||||
|
||||
return attr
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
""" If device should be hidden. """
|
||||
return self.away_hide and self.state != STATE_HOME
|
||||
|
||||
def seen(self, host_name=None, location_name=None, gps=None,
|
||||
gps_accuracy=0, battery=None):
|
||||
""" Mark the device as seen. """
|
||||
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
|
||||
if gps is None:
|
||||
self.gps = None
|
||||
else:
|
||||
try:
|
||||
self.gps = tuple(float(val) for val in gps)
|
||||
except ValueError:
|
||||
_LOGGER.warning('Could not parse gps value for %s: %s',
|
||||
self.dev_id, gps)
|
||||
self.gps = None
|
||||
self.update()
|
||||
|
||||
def stale(self, now=None):
|
||||
""" Return if device state is stale. """
|
||||
return self.last_seen and \
|
||||
(now or dt_util.utcnow()) - self.last_seen > self.consider_home
|
||||
|
||||
def update(self):
|
||||
""" Update state of entity. """
|
||||
if not self.last_seen:
|
||||
return
|
||||
elif self.location_name:
|
||||
self._state = self.location_name
|
||||
elif self.gps is not None:
|
||||
zone_state = zone.active_zone(self.hass, self.gps[0], self.gps[1],
|
||||
self.gps_accuracy)
|
||||
if zone_state is None:
|
||||
self._state = STATE_NOT_HOME
|
||||
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
|
||||
self._state = STATE_HOME
|
||||
else:
|
||||
self._state = zone_state.name
|
||||
|
||||
self.lock.acquire()
|
||||
elif self.stale():
|
||||
self._state = STATE_NOT_HOME
|
||||
self.last_update_home = False
|
||||
else:
|
||||
self._state = STATE_HOME
|
||||
self.last_update_home = True
|
||||
|
||||
self.untracked_devices.clear()
|
||||
|
||||
with open(known_dev_path) as inp:
|
||||
def convert_csv_config(csv_path, yaml_path):
|
||||
""" Convert CSV config file format to YAML. """
|
||||
used_ids = set()
|
||||
with open(csv_path) as inp:
|
||||
for row in csv.DictReader(inp):
|
||||
dev_id = util.ensure_unique_string(
|
||||
(util.slugify(row['name']) or DEVICE_DEFAULT_NAME).lower(),
|
||||
used_ids)
|
||||
used_ids.add(dev_id)
|
||||
device = Device(None, None, None, row['track'] == '1', dev_id,
|
||||
row['device'], row['name'], row['picture'])
|
||||
update_config(yaml_path, dev_id, device)
|
||||
return True
|
||||
|
||||
# To track which devices need an entity_id assigned
|
||||
need_entity_id = []
|
||||
|
||||
# All devices that are still in this set after we read the CSV file
|
||||
# have been removed from the file and thus need to be cleaned up.
|
||||
removed_devices = set(self.tracked.keys())
|
||||
def load_config(path, hass, consider_home, home_range):
|
||||
""" Load devices from YAML config file. """
|
||||
if not os.path.isfile(path):
|
||||
return []
|
||||
return [
|
||||
Device(hass, consider_home, home_range, device.get('track', False),
|
||||
str(dev_id).lower(), str(device.get('mac')).upper(),
|
||||
device.get('name'), device.get('picture'),
|
||||
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
|
||||
for dev_id, device in load_yaml_config_file(path).items()]
|
||||
|
||||
try:
|
||||
for row in csv.DictReader(inp):
|
||||
device = row['device'].upper()
|
||||
|
||||
if row['track'] == '1':
|
||||
if device in self.tracked:
|
||||
# Device exists
|
||||
removed_devices.remove(device)
|
||||
else:
|
||||
# We found a new device
|
||||
need_entity_id.append(device)
|
||||
def setup_scanner_platform(hass, config, scanner, see_device):
|
||||
""" Helper method to connect scanner-based platform to device tracker. """
|
||||
interval = util.convert(config.get(CONF_SCAN_INTERVAL), int,
|
||||
DEFAULT_SCAN_INTERVAL)
|
||||
|
||||
self._track_device(device, row['name'])
|
||||
# Initial scan of each mac we also tell about host name for config
|
||||
seen = set()
|
||||
|
||||
# Update state_attr with latest from file
|
||||
state_attr = {
|
||||
ATTR_FRIENDLY_NAME: row['name']
|
||||
}
|
||||
def device_tracker_scan(now):
|
||||
""" Called when interval matches. """
|
||||
for mac in scanner.scan_devices():
|
||||
if mac in seen:
|
||||
host_name = None
|
||||
else:
|
||||
host_name = scanner.get_device_name(mac)
|
||||
seen.add(mac)
|
||||
see_device(mac=mac, host_name=host_name)
|
||||
|
||||
if row['picture']:
|
||||
state_attr[ATTR_ENTITY_PICTURE] = row['picture']
|
||||
track_utc_time_change(hass, device_tracker_scan, second=range(0, 60,
|
||||
interval))
|
||||
|
||||
self.tracked[device]['state_attr'] = state_attr
|
||||
device_tracker_scan(None)
|
||||
|
||||
else:
|
||||
self.untracked_devices.add(device)
|
||||
|
||||
# Remove existing devices that we no longer track
|
||||
for device in removed_devices:
|
||||
entity_id = self.tracked[device]['entity_id']
|
||||
def update_config(path, dev_id, device):
|
||||
""" Add device to YAML config file. """
|
||||
with open(path, 'a') as out:
|
||||
out.write('\n')
|
||||
out.write('{}:\n'.format(device.dev_id))
|
||||
|
||||
_LOGGER.info("Removing entity %s", entity_id)
|
||||
|
||||
self.hass.states.remove(entity_id)
|
||||
|
||||
self.tracked.pop(device)
|
||||
|
||||
self._generate_entity_ids(need_entity_id)
|
||||
|
||||
if not self.tracked:
|
||||
_LOGGER.warning(
|
||||
"No devices to track. Please update %s.",
|
||||
known_dev_path)
|
||||
|
||||
_LOGGER.info("Loaded devices from %s", known_dev_path)
|
||||
|
||||
except KeyError:
|
||||
self.invalid_known_devices_file = True
|
||||
|
||||
_LOGGER.warning(
|
||||
("Invalid known devices file: %s. "
|
||||
"We won't update it with new found devices."),
|
||||
known_dev_path)
|
||||
|
||||
finally:
|
||||
self.lock.release()
|
||||
|
||||
def _update_known_devices_file(self, new_devices):
|
||||
""" Add new devices to known devices file. """
|
||||
if not self.invalid_known_devices_file:
|
||||
known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE)
|
||||
|
||||
try:
|
||||
# If file does not exist we will write the header too
|
||||
is_new_file = not os.path.isfile(known_dev_path)
|
||||
|
||||
with open(known_dev_path, 'a') as outp:
|
||||
_LOGGER.info("Found %d new devices, updating %s",
|
||||
len(new_devices), known_dev_path)
|
||||
|
||||
writer = csv.writer(outp)
|
||||
|
||||
if is_new_file:
|
||||
writer.writerow(("device", "name", "track", "picture"))
|
||||
|
||||
for device in new_devices:
|
||||
# See if the device scanner knows the name
|
||||
# else defaults to unknown device
|
||||
name = self.device_scanner.get_device_name(device) or \
|
||||
DEVICE_DEFAULT_NAME
|
||||
|
||||
track = 0
|
||||
if self.track_new_devices:
|
||||
self._track_device(device, name)
|
||||
track = 1
|
||||
|
||||
writer.writerow((device, name, track, ""))
|
||||
|
||||
if self.track_new_devices:
|
||||
self._generate_entity_ids(new_devices)
|
||||
|
||||
except IOError:
|
||||
_LOGGER.exception("Error updating %s with %d new devices",
|
||||
known_dev_path, len(new_devices))
|
||||
|
||||
def _track_device(self, device, name):
|
||||
"""
|
||||
Add a device to the list of tracked devices.
|
||||
Does not generate the entity id yet.
|
||||
"""
|
||||
default_last_seen = dt_util.utcnow().replace(year=1990)
|
||||
|
||||
self.tracked[device] = {
|
||||
'name': name,
|
||||
'last_seen': default_last_seen,
|
||||
'state_attr': {ATTR_FRIENDLY_NAME: name}
|
||||
}
|
||||
|
||||
def _generate_entity_ids(self, need_entity_id):
|
||||
""" Generate entity ids for a list of devices. """
|
||||
# Setup entity_ids for the new devices
|
||||
used_entity_ids = [info['entity_id'] for device, info
|
||||
in self.tracked.items()
|
||||
if device not in need_entity_id]
|
||||
|
||||
for device in need_entity_id:
|
||||
name = self.tracked[device]['name']
|
||||
|
||||
entity_id = util.ensure_unique_string(
|
||||
ENTITY_ID_FORMAT.format(util.slugify(name)),
|
||||
used_entity_ids)
|
||||
|
||||
used_entity_ids.append(entity_id)
|
||||
|
||||
self.tracked[device]['entity_id'] = entity_id
|
||||
for key, value in (('name', device.name), ('mac', device.mac),
|
||||
('picture', device.config_picture),
|
||||
('track', 'yes' if device.track else 'no'),
|
||||
(CONF_AWAY_HIDE,
|
||||
'yes' if device.away_hide else 'no')):
|
||||
out.write(' {}: {}\n'.format(key, '' if value is None else value))
|
||||
|
||||
@@ -157,11 +157,19 @@ class AsusWrtDeviceScanner(object):
|
||||
devices = {}
|
||||
for lease in leases_result:
|
||||
match = _LEASES_REGEX.search(lease.decode('utf-8'))
|
||||
|
||||
# For leases where the client doesn't set a hostname, ensure
|
||||
# it is blank and not '*', which breaks the entity_id down
|
||||
# the line
|
||||
host = match.group('host')
|
||||
if host == '*':
|
||||
host = ''
|
||||
|
||||
devices[match.group('ip')] = {
|
||||
'host': host,
|
||||
'status': '',
|
||||
'ip': match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
'host': match.group('host'),
|
||||
'status': ''
|
||||
}
|
||||
|
||||
for neighbor in neighbors:
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.demo
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Demo platform for the device tracker.
|
||||
|
||||
device_tracker:
|
||||
platform: demo
|
||||
"""
|
||||
import random
|
||||
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up a demo tracker. """
|
||||
|
||||
def offset():
|
||||
""" Return random offset. """
|
||||
return (random.randrange(500, 2000)) / 2e5 * random.choice((-1, 1))
|
||||
|
||||
def random_see(dev_id, name):
|
||||
""" Randomize a sighting. """
|
||||
see(
|
||||
dev_id=dev_id,
|
||||
host_name=name,
|
||||
gps=(hass.config.latitude + offset(),
|
||||
hass.config.longitude + offset()),
|
||||
gps_accuracy=random.randrange(50, 150),
|
||||
battery=random.randrange(10, 90)
|
||||
)
|
||||
|
||||
def observe(call=None):
|
||||
""" Observe three entities. """
|
||||
random_see('demo_paulus', 'Paulus')
|
||||
random_see('demo_anne_therese', 'Anne Therese')
|
||||
|
||||
observe()
|
||||
|
||||
see(
|
||||
dev_id='demo_home_boy',
|
||||
host_name='Home Boy',
|
||||
gps=[hass.config.latitude - 0.00002, hass.config.longitude + 0.00002],
|
||||
gps_accuracy=20,
|
||||
battery=53
|
||||
)
|
||||
|
||||
hass.services.register(DOMAIN, 'demo', observe)
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.mqtt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
MQTT platform for the device tracker.
|
||||
|
||||
device_tracker:
|
||||
platform: mqtt
|
||||
qos: 1
|
||||
devices:
|
||||
paulus_oneplus: /location/paulus
|
||||
annetherese_n4: /location/annetherese
|
||||
"""
|
||||
import logging
|
||||
from homeassistant import util
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
CONF_QOS = 'qos'
|
||||
CONF_DEVICES = 'devices'
|
||||
|
||||
DEFAULT_QOS = 0
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up a MQTT tracker. """
|
||||
devices = config.get(CONF_DEVICES)
|
||||
qos = util.convert(config.get(CONF_QOS), int, DEFAULT_QOS)
|
||||
|
||||
if not isinstance(devices, dict):
|
||||
_LOGGER.error('Expected %s to be a dict, found %s', CONF_DEVICES,
|
||||
devices)
|
||||
return False
|
||||
|
||||
dev_id_lookup = {}
|
||||
|
||||
def device_tracker_message_received(topic, payload, qos):
|
||||
""" MQTT message received. """
|
||||
see(dev_id=dev_id_lookup[topic], location_name=payload)
|
||||
|
||||
for dev_id, topic in devices.items():
|
||||
dev_id_lookup[topic] = dev_id
|
||||
mqtt.subscribe(hass, topic, device_tracker_message_received, qos)
|
||||
|
||||
return True
|
||||
@@ -70,7 +70,6 @@ class NetgearDeviceScanner(object):
|
||||
self.lock = threading.Lock()
|
||||
|
||||
if host is None:
|
||||
print("BIER")
|
||||
self._api = pynetgear.Netgear()
|
||||
elif username is None:
|
||||
self._api = pynetgear.Netgear(password, host)
|
||||
|
||||
@@ -44,7 +44,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
# interval in minutes to exclude devices from a scan while they are home
|
||||
CONF_HOME_INTERVAL = "home_interval"
|
||||
|
||||
REQUIREMENTS = ['python-nmap==0.4.1']
|
||||
REQUIREMENTS = ['python-nmap==0.4.3']
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.owntracks
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
OwnTracks platform for the device tracker.
|
||||
|
||||
device_tracker:
|
||||
platform: owntracks
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
LOCATION_TOPIC = 'owntracks/+/+'
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up a OwnTracksks tracker. """
|
||||
|
||||
def owntracks_location_update(topic, payload, qos):
|
||||
""" MQTT message received. """
|
||||
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typelocation
|
||||
try:
|
||||
data = json.loads(payload)
|
||||
except ValueError:
|
||||
# If invalid JSON
|
||||
logging.getLogger(__name__).error(
|
||||
'Unable to parse payload as JSON: %s', payload)
|
||||
return
|
||||
|
||||
if not isinstance(data, dict) or data.get('_type') != 'location':
|
||||
return
|
||||
|
||||
parts = topic.split('/')
|
||||
kwargs = {
|
||||
'dev_id': '{}_{}'.format(parts[1], parts[2]),
|
||||
'host_name': parts[1],
|
||||
'gps': (data['lat'], data['lon']),
|
||||
}
|
||||
if 'acc' in data:
|
||||
kwargs['gps_accuracy'] = data['acc']
|
||||
if 'batt' in data:
|
||||
kwargs['battery'] = data['batt']
|
||||
|
||||
see(**kwargs)
|
||||
|
||||
mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1)
|
||||
|
||||
return True
|
||||
@@ -19,22 +19,22 @@ from homeassistant.const import (
|
||||
|
||||
DOMAIN = "discovery"
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['netdisco==0.3']
|
||||
REQUIREMENTS = ['netdisco==0.4.2']
|
||||
|
||||
SCAN_INTERVAL = 300 # seconds
|
||||
|
||||
# Next 3 lines for now a mirror from netdisco.const
|
||||
# Should setup a mapping netdisco.const -> own constants
|
||||
SERVICE_WEMO = 'belkin_wemo'
|
||||
SERVICE_HUE = 'philips_hue'
|
||||
SERVICE_CAST = 'google_cast'
|
||||
SERVICE_NETGEAR = 'netgear_router'
|
||||
SERVICE_SONOS = 'sonos'
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_WEMO: "switch",
|
||||
SERVICE_CAST: "media_player",
|
||||
SERVICE_HUE: "light",
|
||||
SERVICE_NETGEAR: 'device_tracker',
|
||||
SERVICE_SONOS: 'media_player',
|
||||
}
|
||||
|
||||
|
||||
@@ -79,13 +79,6 @@ def setup(hass, config):
|
||||
if not component:
|
||||
return
|
||||
|
||||
# Hack - fix when device_tracker supports discovery
|
||||
if service == SERVICE_NETGEAR:
|
||||
bootstrap.setup_component(hass, component, {
|
||||
'device_tracker': {'platform': 'netgear'}
|
||||
})
|
||||
return
|
||||
|
||||
# This component cannot be setup.
|
||||
if not bootstrap.setup_component(hass, component, config):
|
||||
return
|
||||
|
||||
@@ -21,7 +21,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
FRONTEND_URLS = [
|
||||
URL_ROOT, '/logbook', '/history', '/devService', '/devState', '/devEvent']
|
||||
URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState',
|
||||
'/devEvent']
|
||||
STATES_URL = re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)')
|
||||
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||
VERSION = "35ecb5457a9ff0f4142c2605b53eb843"
|
||||
VERSION = "c4722afa376379bc4457d54bb9a38cee"
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 797 B |
@@ -12,7 +12,8 @@ from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.util as util
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, STATE_ON, STATE_OFF,
|
||||
STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN)
|
||||
STATE_HOME, STATE_NOT_HOME, STATE_OPEN, STATE_CLOSED,
|
||||
STATE_UNKNOWN)
|
||||
|
||||
DOMAIN = "group"
|
||||
DEPENDENCIES = []
|
||||
@@ -22,7 +23,8 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
ATTR_AUTO = "auto"
|
||||
|
||||
# List of ON/OFF state tuples for groupable states
|
||||
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)]
|
||||
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME),
|
||||
(STATE_OPEN, STATE_CLOSED)]
|
||||
|
||||
|
||||
def _get_group_on_off(state):
|
||||
|
||||
@@ -147,8 +147,6 @@ def _api_history_period(handler, path_match, data):
|
||||
|
||||
end_time = start_time + one_day
|
||||
|
||||
print("Fetchign", start_time, end_time)
|
||||
|
||||
entity_id = data.get('filter_entity_id')
|
||||
|
||||
handler.write_json(
|
||||
|
||||
@@ -205,7 +205,7 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
self.serve_forever()
|
||||
|
||||
def register_path(self, method, url, callback, require_auth=True):
|
||||
""" Registers a path wit the server. """
|
||||
""" Registers a path with the server. """
|
||||
self.paths.append((method, url, callback, require_auth))
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
@@ -232,7 +232,12 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
|
||||
def log_message(self, fmt, *arguments):
|
||||
""" Redirect built-in log to HA logging """
|
||||
_LOGGER.info(fmt, *arguments)
|
||||
if self.server.no_password_set:
|
||||
_LOGGER.info(fmt, *arguments)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
fmt, *(arg.replace(self.server.api_password, '*******')
|
||||
if isinstance(arg, str) else arg for arg in arguments))
|
||||
|
||||
def _handle_request(self, method): # pylint: disable=too-many-branches
|
||||
""" Does some common checks and calls appropriate method. """
|
||||
@@ -487,7 +492,7 @@ class ServerSession:
|
||||
return self._expiry < date_util.utcnow()
|
||||
|
||||
|
||||
class SessionStore:
|
||||
class SessionStore(object):
|
||||
""" Responsible for storing and retrieving http sessions """
|
||||
def __init__(self, enabled=True):
|
||||
""" Set up the session store """
|
||||
|
||||
@@ -52,14 +52,14 @@ import logging
|
||||
import os
|
||||
import csv
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.components import group, discovery, wink, isy994
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||
from homeassistant.components import group, discovery, wink, isy994
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
|
||||
DOMAIN = "light"
|
||||
@@ -275,11 +275,13 @@ def setup(hass, config):
|
||||
light.update_ha_state(True)
|
||||
|
||||
# Listen for light on and light off service calls
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_ON,
|
||||
handle_light_service)
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_light_service,
|
||||
descriptions.get(SERVICE_TURN_ON))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF,
|
||||
handle_light_service)
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_light_service,
|
||||
descriptions.get(SERVICE_TURN_OFF))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -19,11 +19,15 @@ configuration.yaml file.
|
||||
|
||||
light:
|
||||
platform: limitlessled
|
||||
host: 192.168.1.10
|
||||
group_1_name: Living Room
|
||||
group_2_name: Bedroom
|
||||
group_3_name: Office
|
||||
group_4_name: Kitchen
|
||||
bridges:
|
||||
- host: 192.168.1.10
|
||||
group_1_name: Living Room
|
||||
group_2_name: Bedroom
|
||||
group_3_name: Office
|
||||
group_3_type: white
|
||||
group_4_name: Kitchen
|
||||
- host: 192.168.1.11
|
||||
group_2_name: Basement
|
||||
"""
|
||||
import logging
|
||||
|
||||
@@ -33,19 +37,30 @@ from homeassistant.components.light import (Light, ATTR_BRIGHTNESS,
|
||||
from homeassistant.util.color import color_RGB_to_xy
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['ledcontroller==1.0.7']
|
||||
REQUIREMENTS = ['ledcontroller==1.1.0']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Gets the LimitlessLED lights. """
|
||||
import ledcontroller
|
||||
|
||||
led = ledcontroller.LedController(config['host'])
|
||||
# Handle old configuration format:
|
||||
bridges = config.get('bridges', [config])
|
||||
|
||||
for bridge_id, bridge in enumerate(bridges):
|
||||
bridge['id'] = bridge_id
|
||||
|
||||
pool = ledcontroller.LedControllerPool([x['host'] for x in bridges])
|
||||
|
||||
lights = []
|
||||
for i in range(1, 5):
|
||||
if 'group_%d_name' % (i) in config:
|
||||
lights.append(LimitlessLED(led, i, config['group_%d_name' % (i)]))
|
||||
for bridge in bridges:
|
||||
for i in range(1, 5):
|
||||
name_key = 'group_%d_name' % i
|
||||
if name_key in bridge:
|
||||
group_type = bridge.get('group_%d_type' % i, 'rgbw')
|
||||
lights.append(LimitlessLED.factory(pool, bridge['id'], i,
|
||||
bridge[name_key],
|
||||
group_type))
|
||||
|
||||
add_devices_callback(lights)
|
||||
|
||||
@@ -53,15 +68,57 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
class LimitlessLED(Light):
|
||||
""" Represents a LimitlessLED light """
|
||||
|
||||
def __init__(self, led, group, name):
|
||||
self.led = led
|
||||
@staticmethod
|
||||
def factory(pool, controller_id, group, name, group_type):
|
||||
''' Construct a Limitless LED of the appropriate type '''
|
||||
if group_type == 'white':
|
||||
return WhiteLimitlessLED(pool, controller_id, group, name)
|
||||
elif group_type == 'rgbw':
|
||||
return RGBWLimitlessLED(pool, controller_id, group, name)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, pool, controller_id, group, name, group_type):
|
||||
self.pool = pool
|
||||
self.controller_id = controller_id
|
||||
self.group = group
|
||||
|
||||
self.pool.execute(self.controller_id, "set_group_type", self.group,
|
||||
group_type)
|
||||
|
||||
# LimitlessLEDs don't report state, we have track it ourselves.
|
||||
self.led.off(self.group)
|
||||
self.pool.execute(self.controller_id, "off", self.group)
|
||||
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._state = False
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device if any. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return self._state
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
self._state = False
|
||||
self.pool.execute(self.controller_id, "off", self.group)
|
||||
self.update_ha_state()
|
||||
|
||||
|
||||
class RGBWLimitlessLED(LimitlessLED):
|
||||
""" Represents a RGBW LimitlessLED light """
|
||||
|
||||
def __init__(self, pool, controller_id, group, name):
|
||||
super().__init__(pool, controller_id, group, name, 'rgbw')
|
||||
|
||||
self._brightness = 100
|
||||
self._xy_color = color_RGB_to_xy(255, 255, 255)
|
||||
|
||||
@@ -87,16 +144,6 @@ class LimitlessLED(Light):
|
||||
((0xE6, 0xE6, 0xFA), 'lavendar'),
|
||||
]]
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed for a demo light. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device if any. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
return self._brightness
|
||||
@@ -117,11 +164,6 @@ class LimitlessLED(Light):
|
||||
# First candidate in the sorted list is closest to desired color:
|
||||
return sorted(candidates)[0][1]
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
self._state = True
|
||||
@@ -132,12 +174,21 @@ class LimitlessLED(Light):
|
||||
if ATTR_XY_COLOR in kwargs:
|
||||
self._xy_color = kwargs[ATTR_XY_COLOR]
|
||||
|
||||
self.led.set_color(self._xy_to_led_color(self._xy_color), self.group)
|
||||
self.led.set_brightness(self._brightness / 255.0, self.group)
|
||||
self.pool.execute(self.controller_id, "set_color",
|
||||
self._xy_to_led_color(self._xy_color), self.group)
|
||||
self.pool.execute(self.controller_id, "set_brightness",
|
||||
self._brightness / 255.0, self.group)
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
self._state = False
|
||||
self.led.off(self.group)
|
||||
|
||||
class WhiteLimitlessLED(LimitlessLED):
|
||||
""" Represents a White LimitlessLED light """
|
||||
|
||||
def __init__(self, pool, controller_id, group, name):
|
||||
super().__init__(pool, controller_id, group, name, 'white')
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
self._state = True
|
||||
self.pool.execute(self.controller_id, "on", self.group)
|
||||
self.update_ha_state()
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# Describes the format for available light services
|
||||
|
||||
turn_on:
|
||||
description: Turn a light on
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to turn on
|
||||
example: 'light.kitchen'
|
||||
|
||||
transition:
|
||||
description: Duration in seconds it takes to get to next state
|
||||
example: 60
|
||||
|
||||
rgb_color:
|
||||
description: Color for the light in RGB-format
|
||||
example: '[255, 100, 100]'
|
||||
|
||||
xy_color:
|
||||
description: Color for the light in XY-format
|
||||
example: '[0.52, 0.43]'
|
||||
|
||||
brightness:
|
||||
description: Number between 0..255 indicating brightness
|
||||
example: 120
|
||||
|
||||
profile:
|
||||
description: Name of a light profile to use
|
||||
example: relax
|
||||
|
||||
flash:
|
||||
description: If the light should flash
|
||||
values:
|
||||
- short
|
||||
- long
|
||||
|
||||
effect:
|
||||
description: Light effect
|
||||
values:
|
||||
- colorloop
|
||||
|
||||
turn_off:
|
||||
description: Turn a light off
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to turn off
|
||||
example: 'light.kitchen'
|
||||
|
||||
transition:
|
||||
description: Duration in seconds it takes to get to next state
|
||||
example: 60
|
||||
@@ -6,12 +6,14 @@ Support for Tellstick lights.
|
||||
import logging
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
from homeassistant.components.light import Light, ATTR_BRIGHTNESS
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP,
|
||||
ATTR_FRIENDLY_NAME)
|
||||
import tellcore.constants as tellcore_constants
|
||||
|
||||
REQUIREMENTS = ['tellcore-py==1.0.4']
|
||||
from tellcore.library import DirectCallbackDispatcher
|
||||
REQUIREMENTS = ['tellcore-py==1.1.2']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Find and return Tellstick lights. """
|
||||
|
||||
@@ -22,13 +24,32 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"Failed to import tellcore")
|
||||
return []
|
||||
|
||||
core = telldus.TelldusCore()
|
||||
core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher())
|
||||
|
||||
switches_and_lights = core.devices()
|
||||
lights = []
|
||||
|
||||
for switch in switches_and_lights:
|
||||
if switch.methods(tellcore_constants.TELLSTICK_DIM):
|
||||
lights.append(TellstickLight(switch))
|
||||
|
||||
def _device_event_callback(id_, method, data, cid):
|
||||
""" Called from the TelldusCore library to update one device """
|
||||
for light_device in lights:
|
||||
if light_device.tellstick_device.id == id_:
|
||||
# Execute the update in another thread
|
||||
light_device.update_ha_state(True)
|
||||
break
|
||||
|
||||
callback_id = core.register_device_event(_device_event_callback)
|
||||
|
||||
def unload_telldus_lib(event):
|
||||
""" Un-register the callback bindings """
|
||||
if callback_id is not None:
|
||||
core.unregister_callback(callback_id)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, unload_telldus_lib)
|
||||
|
||||
add_devices_callback(lights)
|
||||
|
||||
|
||||
@@ -40,15 +61,15 @@ class TellstickLight(Light):
|
||||
tellcore_constants.TELLSTICK_UP |
|
||||
tellcore_constants.TELLSTICK_DOWN)
|
||||
|
||||
def __init__(self, tellstick):
|
||||
self.tellstick = tellstick
|
||||
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
|
||||
def __init__(self, tellstick_device):
|
||||
self.tellstick_device = tellstick_device
|
||||
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name}
|
||||
self._brightness = 0
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the switch if any. """
|
||||
return self.tellstick.name
|
||||
return self.tellstick_device.name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@@ -62,8 +83,9 @@ class TellstickLight(Light):
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turns the switch off. """
|
||||
self.tellstick.turn_off()
|
||||
self.tellstick_device.turn_off()
|
||||
self._brightness = 0
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
@@ -74,11 +96,12 @@ class TellstickLight(Light):
|
||||
else:
|
||||
self._brightness = brightness
|
||||
|
||||
self.tellstick.dim(self._brightness)
|
||||
self.tellstick_device.dim(self._brightness)
|
||||
self.update_ha_state()
|
||||
|
||||
def update(self):
|
||||
""" Update state of the light. """
|
||||
last_command = self.tellstick.last_sent_command(
|
||||
last_command = self.tellstick_device.last_sent_command(
|
||||
self.last_sent_command_mask)
|
||||
|
||||
if last_command == tellcore_constants.TELLSTICK_TURNON:
|
||||
@@ -88,6 +111,11 @@ class TellstickLight(Light):
|
||||
elif (last_command == tellcore_constants.TELLSTICK_DIM or
|
||||
last_command == tellcore_constants.TELLSTICK_UP or
|
||||
last_command == tellcore_constants.TELLSTICK_DOWN):
|
||||
last_sent_value = self.tellstick.last_sent_value()
|
||||
last_sent_value = self.tellstick_device.last_sent_value()
|
||||
if last_sent_value is not None:
|
||||
self._brightness = last_sent_value
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Tells Home Assistant not to poll this entity. """
|
||||
return False
|
||||
|
||||
@@ -10,11 +10,12 @@ import re
|
||||
|
||||
from homeassistant.core import State, DOMAIN as HA_DOMAIN
|
||||
from homeassistant.const import (
|
||||
EVENT_STATE_CHANGED, STATE_HOME, STATE_ON, STATE_OFF,
|
||||
EVENT_STATE_CHANGED, STATE_NOT_HOME, STATE_ON, STATE_OFF,
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST)
|
||||
from homeassistant import util
|
||||
import homeassistant.util.dt as dt_util
|
||||
import homeassistant.components.recorder as recorder
|
||||
import homeassistant.components.sun as sun
|
||||
from homeassistant.components import recorder, sun
|
||||
|
||||
|
||||
DOMAIN = "logbook"
|
||||
DEPENDENCIES = ['recorder', 'http']
|
||||
@@ -25,8 +26,29 @@ QUERY_EVENTS_BETWEEN = """
|
||||
SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
|
||||
"""
|
||||
|
||||
EVENT_LOGBOOK_ENTRY = 'LOGBOOK_ENTRY'
|
||||
|
||||
GROUP_BY_MINUTES = 15
|
||||
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_MESSAGE = 'message'
|
||||
ATTR_DOMAIN = 'domain'
|
||||
ATTR_ENTITY_ID = 'entity_id'
|
||||
|
||||
|
||||
def log_entry(hass, name, message, domain=None, entity_id=None):
|
||||
""" Adds an entry to the logbook. """
|
||||
data = {
|
||||
ATTR_NAME: name,
|
||||
ATTR_MESSAGE: message
|
||||
}
|
||||
|
||||
if domain is not None:
|
||||
data[ATTR_DOMAIN] = domain
|
||||
if entity_id is not None:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
hass.bus.fire(EVENT_LOGBOOK_ENTRY, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Listens for download events to download files. """
|
||||
@@ -110,7 +132,10 @@ def humanify(events):
|
||||
# Process events
|
||||
for event in events_batch:
|
||||
if event.event_type == EVENT_STATE_CHANGED:
|
||||
entity_id = event.data['entity_id']
|
||||
entity_id = event.data.get('entity_id')
|
||||
|
||||
if entity_id is None:
|
||||
continue
|
||||
|
||||
if entity_id.startswith('sensor.'):
|
||||
last_sensor_event[entity_id] = event
|
||||
@@ -137,10 +162,12 @@ def humanify(events):
|
||||
|
||||
to_state = State.from_dict(event.data.get('new_state'))
|
||||
|
||||
# if last_changed == last_updated only attributes have changed
|
||||
# we do not report on that yet.
|
||||
# if last_changed != last_updated only attributes have changed
|
||||
# we do not report on that yet. Also filter auto groups.
|
||||
if not to_state or \
|
||||
to_state.last_changed != to_state.last_updated:
|
||||
to_state.last_changed != to_state.last_updated or \
|
||||
to_state.domain == 'group' and \
|
||||
to_state.attributes.get('auto', False):
|
||||
continue
|
||||
|
||||
domain = to_state.domain
|
||||
@@ -175,14 +202,31 @@ def humanify(events):
|
||||
event.time_fired, "Home Assistant", action,
|
||||
domain=HA_DOMAIN)
|
||||
|
||||
elif event.event_type == EVENT_LOGBOOK_ENTRY:
|
||||
domain = event.data.get(ATTR_DOMAIN)
|
||||
entity_id = event.data.get(ATTR_ENTITY_ID)
|
||||
if domain is None and entity_id is not None:
|
||||
try:
|
||||
domain = util.split_entity_id(str(entity_id))[0]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
yield Entry(
|
||||
event.time_fired, event.data.get(ATTR_NAME),
|
||||
event.data.get(ATTR_MESSAGE), domain,
|
||||
entity_id)
|
||||
|
||||
|
||||
def _entry_message_from_state(domain, state):
|
||||
""" Convert a state to a message for the logbook. """
|
||||
# We pass domain in so we don't have to split entity_id again
|
||||
# pylint: disable=too-many-return-statements
|
||||
|
||||
if domain == 'device_tracker':
|
||||
return '{} home'.format(
|
||||
'arrived' if state.state == STATE_HOME else 'left')
|
||||
if state.state == STATE_NOT_HOME:
|
||||
return 'is away'
|
||||
else:
|
||||
return 'is at {}'.format(state.state)
|
||||
|
||||
elif domain == 'sun':
|
||||
if state.state == sun.STATE_ABOVE_HORIZON:
|
||||
|
||||
@@ -5,8 +5,10 @@ homeassistant.components.media_player
|
||||
Component to interface with various media players.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.components import discovery
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.const import (
|
||||
@@ -19,12 +21,13 @@ from homeassistant.const import (
|
||||
|
||||
DOMAIN = 'media_player'
|
||||
DEPENDENCIES = []
|
||||
SCAN_INTERVAL = 30
|
||||
SCAN_INTERVAL = 10
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
discovery.SERVICE_CAST: 'cast',
|
||||
discovery.SERVICE_SONOS: 'sonos',
|
||||
}
|
||||
|
||||
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video'
|
||||
@@ -185,6 +188,9 @@ def setup(hass, config):
|
||||
|
||||
component.setup(config)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def media_player_service_handler(service):
|
||||
""" Maps services to methods on MediaPlayerDevice. """
|
||||
target_players = component.extract_from_service(service)
|
||||
@@ -198,7 +204,8 @@ def setup(hass, config):
|
||||
player.update_ha_state(True)
|
||||
|
||||
for service in SERVICE_TO_METHOD:
|
||||
hass.services.register(DOMAIN, service, media_player_service_handler)
|
||||
hass.services.register(DOMAIN, service, media_player_service_handler,
|
||||
descriptions.get(service))
|
||||
|
||||
def volume_set_service(service):
|
||||
""" Set specified volume on the media player. """
|
||||
@@ -215,7 +222,8 @@ def setup(hass, config):
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service)
|
||||
hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service,
|
||||
descriptions.get(SERVICE_VOLUME_SET))
|
||||
|
||||
def volume_mute_service(service):
|
||||
""" Mute (true) or unmute (false) the media player. """
|
||||
@@ -232,7 +240,8 @@ def setup(hass, config):
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, volume_mute_service)
|
||||
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, volume_mute_service,
|
||||
descriptions.get(SERVICE_VOLUME_MUTE))
|
||||
|
||||
def media_seek_service(service):
|
||||
""" Seek to a position. """
|
||||
@@ -249,7 +258,8 @@ def setup(hass, config):
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service)
|
||||
hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service,
|
||||
descriptions.get(SERVICE_MEDIA_SEEK))
|
||||
|
||||
def play_youtube_video_service(service, media_id=None):
|
||||
""" Plays specified media_id on the media player. """
|
||||
@@ -267,14 +277,17 @@ def setup(hass, config):
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, "start_fireplace",
|
||||
lambda service: play_youtube_video_service(service, "eyU3bRy2x44"))
|
||||
lambda service: play_youtube_video_service(service, "eyU3bRy2x44"),
|
||||
descriptions.get('start_fireplace'))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, "start_epic_sax",
|
||||
lambda service: play_youtube_video_service(service, "kxopViU98Xo"))
|
||||
lambda service: play_youtube_video_service(service, "kxopViU98Xo"),
|
||||
descriptions.get('start_epic_sax'))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service)
|
||||
DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service,
|
||||
descriptions.get(SERVICE_YOUTUBE_VIDEO))
|
||||
|
||||
return True
|
||||
|
||||
@@ -483,7 +496,7 @@ class MediaPlayerDevice(Entity):
|
||||
else:
|
||||
state_attr = {
|
||||
attr: getattr(self, attr) for attr
|
||||
in ATTR_TO_PROPERTY if getattr(self, attr)
|
||||
in ATTR_TO_PROPERTY if getattr(self, attr) is not None
|
||||
}
|
||||
|
||||
if self.media_image_url:
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
"""
|
||||
homeassistant.components.media_player.itunes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides an interface to iTunes-API (https://github.com/maddox/itunes-api)
|
||||
|
||||
The iTunes media player will allow you to control your iTunes instance. You
|
||||
can play/pause/next/previous/mute, adjust volume, etc.
|
||||
|
||||
In addition to controlling iTunes, your available AirPlay endpoints will be
|
||||
added as media players as well. You can then individually address them append
|
||||
turn them on, turn them off, or adjust their volume.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use iTunes you will need to add something like the following to
|
||||
your configuration.yaml file.
|
||||
|
||||
media_player:
|
||||
platform: itunes
|
||||
name: iTunes
|
||||
host: http://192.168.1.16
|
||||
port: 8181
|
||||
|
||||
Variables:
|
||||
|
||||
name
|
||||
*Optional
|
||||
The name of the device.
|
||||
|
||||
url
|
||||
*Required
|
||||
URL of your running version of iTunes-API. Example: http://192.168.1.50:8181
|
||||
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice, MEDIA_TYPE_MUSIC, SUPPORT_PAUSE, SUPPORT_SEEK,
|
||||
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
|
||||
ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_MEDIA_COMMANDS)
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_ON)
|
||||
|
||||
import requests
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_ITUNES = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK
|
||||
|
||||
SUPPORT_AIRPLAY = SUPPORT_VOLUME_SET | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
||||
|
||||
DOMAIN = 'itunes'
|
||||
|
||||
|
||||
class Itunes(object):
|
||||
""" itunes-api client. """
|
||||
|
||||
def __init__(self, host, port):
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
@property
|
||||
def _base_url(self):
|
||||
""" Returns the base url for endpoints. """
|
||||
return self.host + ":" + str(self.port)
|
||||
|
||||
def _request(self, method, path, params=None):
|
||||
""" Makes the actual request and returns the parsed response. """
|
||||
url = self._base_url + path
|
||||
|
||||
try:
|
||||
if method == 'GET':
|
||||
response = requests.get(url)
|
||||
elif method == "POST":
|
||||
response = requests.put(url, params)
|
||||
elif method == "PUT":
|
||||
response = requests.put(url, params)
|
||||
elif method == "DELETE":
|
||||
response = requests.delete(url)
|
||||
|
||||
return response.json()
|
||||
except requests.exceptions.HTTPError:
|
||||
return {'player_state': 'error'}
|
||||
except requests.exceptions.RequestException:
|
||||
return {'player_state': 'offline'}
|
||||
|
||||
def _command(self, named_command):
|
||||
""" Makes a request for a controlling command. """
|
||||
return self._request('PUT', '/' + named_command)
|
||||
|
||||
def now_playing(self):
|
||||
""" Returns the current state. """
|
||||
return self._request('GET', '/now_playing')
|
||||
|
||||
def set_volume(self, level):
|
||||
""" Sets the volume and returns the current state, level 0-100. """
|
||||
return self._request('PUT', '/volume', {'level': level})
|
||||
|
||||
def set_muted(self, muted):
|
||||
""" Mutes and returns the current state, muted True or False. """
|
||||
return self._request('PUT', '/mute', {'muted': muted})
|
||||
|
||||
def play(self):
|
||||
""" Sets playback to play and returns the current state. """
|
||||
return self._command('play')
|
||||
|
||||
def pause(self):
|
||||
""" Sets playback to paused and returns the current state. """
|
||||
return self._command('pause')
|
||||
|
||||
def next(self):
|
||||
""" Skips to the next track and returns the current state. """
|
||||
return self._command('next')
|
||||
|
||||
def previous(self):
|
||||
""" Skips back and returns the current state. """
|
||||
return self._command('previous')
|
||||
|
||||
def artwork_url(self):
|
||||
""" Returns a URL of the current track's album art. """
|
||||
return self._base_url + '/artwork'
|
||||
|
||||
def airplay_devices(self):
|
||||
""" Returns a list of AirPlay devices. """
|
||||
return self._request('GET', '/airplay_devices')
|
||||
|
||||
def airplay_device(self, device_id):
|
||||
""" Returns an AirPlay device. """
|
||||
return self._request('GET', '/airplay_devices/' + device_id)
|
||||
|
||||
def toggle_airplay_device(self, device_id, toggle):
|
||||
""" Toggles airplay device on or off, id, toggle True or False. """
|
||||
command = 'on' if toggle else 'off'
|
||||
path = '/airplay_devices/' + device_id + '/' + command
|
||||
return self._request('PUT', path)
|
||||
|
||||
def set_volume_airplay_device(self, device_id, level):
|
||||
""" Sets volume, returns current state of device, id,level 0-100. """
|
||||
path = '/airplay_devices/' + device_id + '/volume'
|
||||
return self._request('PUT', path, {'level': level})
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=abstract-method
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the itunes platform. """
|
||||
|
||||
add_devices([
|
||||
ItunesDevice(
|
||||
config.get('name', 'iTunes'),
|
||||
config.get('host'),
|
||||
config.get('port'),
|
||||
add_devices
|
||||
)
|
||||
])
|
||||
|
||||
|
||||
class ItunesDevice(MediaPlayerDevice):
|
||||
""" Represents a iTunes-API instance. """
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
def __init__(self, name, host, port, add_devices):
|
||||
self._name = name
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._add_devices = add_devices
|
||||
|
||||
self.client = Itunes(self._host, self._port)
|
||||
|
||||
self.current_volume = None
|
||||
self.muted = None
|
||||
self.current_title = None
|
||||
self.current_album = None
|
||||
self.current_artist = None
|
||||
self.current_playlist = None
|
||||
self.content_id = None
|
||||
|
||||
self.player_state = None
|
||||
|
||||
self.airplay_devices = {}
|
||||
|
||||
self.update()
|
||||
|
||||
def update_state(self, state_hash):
|
||||
""" Update all the state properties with the passed in dictionary. """
|
||||
self.player_state = state_hash.get('player_state', None)
|
||||
|
||||
self.current_volume = state_hash.get('volume', 0)
|
||||
self.muted = state_hash.get('muted', None)
|
||||
self.current_title = state_hash.get('name', None)
|
||||
self.current_album = state_hash.get('album', None)
|
||||
self.current_artist = state_hash.get('artist', None)
|
||||
self.current_playlist = state_hash.get('playlist', None)
|
||||
self.content_id = state_hash.get('id', None)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
|
||||
if self.player_state == 'offline' or self.player_state is None:
|
||||
return 'offline'
|
||||
|
||||
if self.player_state == 'error':
|
||||
return 'error'
|
||||
|
||||
if self.player_state == 'stopped':
|
||||
return STATE_IDLE
|
||||
|
||||
if self.player_state == 'paused':
|
||||
return STATE_PAUSED
|
||||
else:
|
||||
return STATE_PLAYING
|
||||
|
||||
def update(self):
|
||||
""" Retrieve latest state. """
|
||||
now_playing = self.client.now_playing()
|
||||
self.update_state(now_playing)
|
||||
|
||||
found_devices = self.client.airplay_devices()
|
||||
found_devices = found_devices.get('airplay_devices', [])
|
||||
|
||||
new_devices = []
|
||||
|
||||
for device_data in found_devices:
|
||||
device_id = device_data.get('id')
|
||||
|
||||
if self.airplay_devices.get(device_id):
|
||||
# update it
|
||||
airplay_device = self.airplay_devices.get(device_id)
|
||||
airplay_device.update_state(device_data)
|
||||
else:
|
||||
# add it
|
||||
airplay_device = AirPlayDevice(device_id, self.client)
|
||||
airplay_device.update_state(device_data)
|
||||
self.airplay_devices[device_id] = airplay_device
|
||||
new_devices.append(airplay_device)
|
||||
|
||||
if new_devices:
|
||||
self._add_devices(new_devices)
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
""" Boolean if volume is currently muted. """
|
||||
return self.muted
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
""" Volume level of the media player (0..1). """
|
||||
return self.current_volume/100.0
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
""" Content ID of current playing media. """
|
||||
return self.content_id
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
""" Content type of current playing media. """
|
||||
return MEDIA_TYPE_MUSIC
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
""" Image url of current playing media. """
|
||||
|
||||
if self.player_state in (STATE_PLAYING, STATE_IDLE, STATE_PAUSED) and \
|
||||
self.current_title is not None:
|
||||
return self.client.artwork_url()
|
||||
else:
|
||||
return 'https://cloud.githubusercontent.com/assets/260/9829355' \
|
||||
'/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png'
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
""" Title of current playing media. """
|
||||
return self.current_title
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
""" Artist of current playing media. (Music track only) """
|
||||
return self.current_artist
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
""" Album of current playing media. (Music track only) """
|
||||
return self.current_album
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
""" Flags of media commands that are supported. """
|
||||
return SUPPORT_ITUNES
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
""" set volume level, range 0..1. """
|
||||
response = self.client.set_volume(int(volume * 100))
|
||||
self.update_state(response)
|
||||
|
||||
def mute_volume(self, mute):
|
||||
""" mute (true) or unmute (false) media player. """
|
||||
response = self.client.set_muted(mute)
|
||||
self.update_state(response)
|
||||
|
||||
def media_play(self):
|
||||
""" media_play media player. """
|
||||
response = self.client.play()
|
||||
self.update_state(response)
|
||||
|
||||
def media_pause(self):
|
||||
""" media_pause media player. """
|
||||
response = self.client.pause()
|
||||
self.update_state(response)
|
||||
|
||||
def media_next_track(self):
|
||||
""" media_next media player. """
|
||||
response = self.client.next()
|
||||
self.update_state(response)
|
||||
|
||||
def media_previous_track(self):
|
||||
""" media_previous media player. """
|
||||
response = self.client.previous()
|
||||
self.update_state(response)
|
||||
|
||||
|
||||
class AirPlayDevice(MediaPlayerDevice):
|
||||
""" Represents an AirPlay device via an iTunes-API instance. """
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
def __init__(self, device_id, client):
|
||||
self._id = device_id
|
||||
|
||||
self.client = client
|
||||
|
||||
self.device_name = "AirPlay"
|
||||
self.kind = None
|
||||
self.active = False
|
||||
self.selected = False
|
||||
self.volume = 0
|
||||
self.supports_audio = False
|
||||
self.supports_video = False
|
||||
self.player_state = None
|
||||
|
||||
def update_state(self, state_hash):
|
||||
""" Update all the state properties with the passed in dictionary. """
|
||||
|
||||
if 'player_state' in state_hash:
|
||||
self.player_state = state_hash.get('player_state', None)
|
||||
|
||||
if 'name' in state_hash:
|
||||
name = state_hash.get('name', '')
|
||||
self.device_name = (name + ' AirTunes Speaker').strip()
|
||||
|
||||
if 'kind' in state_hash:
|
||||
self.kind = state_hash.get('kind', None)
|
||||
|
||||
if 'active' in state_hash:
|
||||
self.active = state_hash.get('active', None)
|
||||
|
||||
if 'selected' in state_hash:
|
||||
self.selected = state_hash.get('selected', None)
|
||||
|
||||
if 'sound_volume' in state_hash:
|
||||
self.volume = state_hash.get('sound_volume', 0)
|
||||
|
||||
if 'supports_audio' in state_hash:
|
||||
self.supports_audio = state_hash.get('supports_audio', None)
|
||||
|
||||
if 'supports_video' in state_hash:
|
||||
self.supports_video = state_hash.get('supports_video', None)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self.device_name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
|
||||
if self.selected is True:
|
||||
return STATE_ON
|
||||
else:
|
||||
return STATE_OFF
|
||||
|
||||
def update(self):
|
||||
""" Retrieve latest state. """
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
return float(self.volume)/100.0
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
return MEDIA_TYPE_MUSIC
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
""" Flags of media commands that are supported. """
|
||||
return SUPPORT_AIRPLAY
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Return the state attributes. """
|
||||
state_attr = {}
|
||||
state_attr[ATTR_SUPPORTED_MEDIA_COMMANDS] = SUPPORT_AIRPLAY
|
||||
|
||||
if self.state == STATE_OFF:
|
||||
state_attr[ATTR_ENTITY_PICTURE] = \
|
||||
('https://cloud.githubusercontent.com/assets/260/9833073'
|
||||
'/6eb5c906-5958-11e5-9b4a-472cdf36be16.png')
|
||||
else:
|
||||
state_attr[ATTR_ENTITY_PICTURE] = \
|
||||
('https://cloud.githubusercontent.com/assets/260/9833072'
|
||||
'/6eb13cce-5958-11e5-996f-e2aaefbc9a24.png')
|
||||
|
||||
return state_attr
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
""" set volume level, range 0..1. """
|
||||
volume = int(volume * 100)
|
||||
response = self.client.set_volume_airplay_device(self._id, volume)
|
||||
self.update_state(response)
|
||||
|
||||
def turn_on(self):
|
||||
""" Select AirPlay. """
|
||||
self.update_state({"selected": True})
|
||||
self.update_ha_state()
|
||||
response = self.client.toggle_airplay_device(self._id, True)
|
||||
self.update_state(response)
|
||||
|
||||
def turn_off(self):
|
||||
""" Deselect AirPlay. """
|
||||
self.update_state({"selected": False})
|
||||
self.update_ha_state()
|
||||
response = self.client.toggle_airplay_device(self._id, False)
|
||||
self.update_state(response)
|
||||
@@ -107,6 +107,7 @@ class KodiDevice(MediaPlayerDevice):
|
||||
try:
|
||||
return self._server.Player.GetActivePlayers()
|
||||
except jsonrpc_requests.jsonrpc.TransportError:
|
||||
_LOGGER.exception('Unable to fetch kodi data')
|
||||
return None
|
||||
|
||||
@property
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
"""
|
||||
homeassistant.components.media_player.plex
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides an interface to the Plex API
|
||||
|
||||
Configuration:
|
||||
|
||||
To use Plex add something like this to your configuration:
|
||||
|
||||
media_player:
|
||||
platform: plex
|
||||
name: plex_server
|
||||
user: plex
|
||||
password: my_secure_password
|
||||
|
||||
Variables:
|
||||
|
||||
name
|
||||
*Required
|
||||
The name of the backend device (Under Plex Media Server > settings > server).
|
||||
|
||||
user
|
||||
*Required
|
||||
The Plex username
|
||||
|
||||
password
|
||||
*Required
|
||||
The Plex password
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_NEXT_TRACK, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_UNKNOWN)
|
||||
import homeassistant.util as util
|
||||
|
||||
REQUIREMENTS = ['https://github.com/adrienbrault/python-plexapi/archive/'
|
||||
'df2d0847e801d6d5cda920326d693cf75f304f1a.zip'
|
||||
'#python-plexapi==1.0.2']
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the plex platform. """
|
||||
from plexapi.myplex import MyPlexUser
|
||||
from plexapi.exceptions import BadRequest
|
||||
|
||||
name = config.get('name', '')
|
||||
user = config.get('user', '')
|
||||
password = config.get('password', '')
|
||||
plexuser = MyPlexUser.signin(user, password)
|
||||
plexserver = plexuser.getResource(name).connect()
|
||||
plex_clients = {}
|
||||
plex_sessions = {}
|
||||
|
||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||
def update_devices():
|
||||
""" Updates the devices objects """
|
||||
try:
|
||||
devices = plexuser.devices()
|
||||
except BadRequest:
|
||||
_LOGGER.exception("Error listing plex devices")
|
||||
return
|
||||
|
||||
new_plex_clients = []
|
||||
for device in devices:
|
||||
if (all(x not in ['client', 'player'] for x in device.provides)
|
||||
or 'PlexAPI' == device.product):
|
||||
continue
|
||||
|
||||
if device.clientIdentifier not in plex_clients:
|
||||
new_client = PlexClient(device, plex_sessions, update_devices,
|
||||
update_sessions)
|
||||
plex_clients[device.clientIdentifier] = new_client
|
||||
new_plex_clients.append(new_client)
|
||||
else:
|
||||
plex_clients[device.clientIdentifier].set_device(device)
|
||||
|
||||
if new_plex_clients:
|
||||
add_devices(new_plex_clients)
|
||||
|
||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||
def update_sessions():
|
||||
""" Updates the sessions objects """
|
||||
try:
|
||||
sessions = plexserver.sessions()
|
||||
except BadRequest:
|
||||
_LOGGER.exception("Error listing plex sessions")
|
||||
return
|
||||
|
||||
plex_sessions.clear()
|
||||
for session in sessions:
|
||||
plex_sessions[session.player.machineIdentifier] = session
|
||||
|
||||
update_devices()
|
||||
update_sessions()
|
||||
|
||||
|
||||
class PlexClient(MediaPlayerDevice):
|
||||
""" Represents a Plex device. """
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
def __init__(self, device, plex_sessions, update_devices, update_sessions):
|
||||
self.plex_sessions = plex_sessions
|
||||
self.update_devices = update_devices
|
||||
self.update_sessions = update_sessions
|
||||
self.set_device(device)
|
||||
|
||||
def set_device(self, device):
|
||||
""" Sets the device property """
|
||||
self.device = device
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
""" Returns the session, if any """
|
||||
if self.device.clientIdentifier not in self.plex_sessions:
|
||||
return None
|
||||
|
||||
return self.plex_sessions[self.device.clientIdentifier]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self.device.name or self.device.product or self.device.device
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
if self.session:
|
||||
state = self.session.player.state
|
||||
if state == 'playing':
|
||||
return STATE_PLAYING
|
||||
elif state == 'paused':
|
||||
return STATE_PAUSED
|
||||
elif self.device.isReachable:
|
||||
return STATE_IDLE
|
||||
else:
|
||||
return STATE_OFF
|
||||
|
||||
return STATE_UNKNOWN
|
||||
|
||||
def update(self):
|
||||
self.update_devices(no_throttle=True)
|
||||
self.update_sessions(no_throttle=True)
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
""" Content ID of current playing media. """
|
||||
if self.session is not None:
|
||||
return self.session.ratingKey
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
""" Content type of current playing media. """
|
||||
if self.session is None:
|
||||
return None
|
||||
media_type = self.session.type
|
||||
if media_type == 'episode':
|
||||
return MEDIA_TYPE_TVSHOW
|
||||
elif media_type == 'movie':
|
||||
return MEDIA_TYPE_VIDEO
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
""" Duration of current playing media in seconds. """
|
||||
if self.session is not None:
|
||||
return self.session.duration
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
""" Image url of current playing media. """
|
||||
if self.session is not None:
|
||||
return self.session.thumbUrl
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
""" Title of current playing media. """
|
||||
# find a string we can use as a title
|
||||
if self.session is not None:
|
||||
return self.session.title
|
||||
|
||||
@property
|
||||
def media_season(self):
|
||||
""" Season of curent playing media. (TV Show only) """
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self.session.seasons()[0].index
|
||||
|
||||
@property
|
||||
def media_series_title(self):
|
||||
""" Series title of current playing media. (TV Show only)"""
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self.session.grandparentTitle
|
||||
|
||||
@property
|
||||
def media_episode(self):
|
||||
""" Episode of current playing media. (TV Show only) """
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self.session.index
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
""" Flags of media commands that are supported. """
|
||||
return SUPPORT_PLEX
|
||||
|
||||
def media_play(self):
|
||||
""" media_play media player. """
|
||||
self.device.play({'type': 'video'})
|
||||
|
||||
def media_pause(self):
|
||||
""" media_pause media player. """
|
||||
self.device.pause({'type': 'video'})
|
||||
|
||||
def media_next_track(self):
|
||||
""" Send next track command. """
|
||||
self.device.skipNext({'type': 'video'})
|
||||
|
||||
def media_previous_track(self):
|
||||
""" Send previous track command. """
|
||||
self.device.skipPrevious({'type': 'video'})
|
||||
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
homeassistant.components.media_player.sonos
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides an interface to Sonos players (via SoCo)
|
||||
|
||||
Configuration:
|
||||
|
||||
To use SoCo, add something like this to your configuration:
|
||||
|
||||
media_player:
|
||||
platform: sonos
|
||||
"""
|
||||
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET,
|
||||
SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
|
||||
MEDIA_TYPE_MUSIC)
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN)
|
||||
|
||||
|
||||
REQUIREMENTS = ['SoCo==0.11.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# The soco library is excessively chatty when it comes to logging and
|
||||
# causes a LOT of spam in the logs due to making a http connection to each
|
||||
# speaker every 10 seconds. Quiet it down a bit to just actual problems.
|
||||
_SOCO_LOGGER = logging.getLogger('soco')
|
||||
_SOCO_LOGGER.setLevel(logging.ERROR)
|
||||
_REQUESTS_LOGGER = logging.getLogger('requests')
|
||||
_REQUESTS_LOGGER.setLevel(logging.ERROR)
|
||||
|
||||
SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Sonos platform. """
|
||||
import soco
|
||||
|
||||
players = soco.discover()
|
||||
if not players:
|
||||
_LOGGER.warning('No Sonos speakers found. Disabling: %s', __name__)
|
||||
return False
|
||||
|
||||
add_devices(SonosDevice(hass, p) for p in players)
|
||||
_LOGGER.info('Added %s Sonos speakers', len(players))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
# pylint: disable=too-many-public-methods
|
||||
# pylint: disable=abstract-method
|
||||
class SonosDevice(MediaPlayerDevice):
|
||||
""" Represents a Sonos device. """
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, player):
|
||||
self.hass = hass
|
||||
super(SonosDevice, self).__init__()
|
||||
self._player = player
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
return True
|
||||
|
||||
def update_sonos(self, now):
|
||||
""" Updates state, called by track_utc_time_change """
|
||||
self.update_ha_state(True)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
""" Returns a unique id. """
|
||||
return "{}.{}".format(self.__class__, self._player.uid)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
if self._status == 'PAUSED_PLAYBACK':
|
||||
return STATE_PAUSED
|
||||
if self._status == 'PLAYING':
|
||||
return STATE_PLAYING
|
||||
if self._status == 'STOPPED':
|
||||
return STATE_IDLE
|
||||
return STATE_UNKNOWN
|
||||
|
||||
def update(self):
|
||||
""" Retrieve latest state. """
|
||||
self._name = self._player.get_speaker_info()['zone_name'].replace(
|
||||
' (R)', '').replace(' (L)', '')
|
||||
self._status = self._player.get_current_transport_info().get(
|
||||
'current_transport_state')
|
||||
self._trackinfo = self._player.get_current_track_info()
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
""" Volume level of the media player (0..1). """
|
||||
return self._player.volume / 100.0
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
return self._player.mute
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
""" Content ID of current playing media. """
|
||||
return self._trackinfo.get('title', None)
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
""" Content type of current playing media. """
|
||||
return MEDIA_TYPE_MUSIC
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
""" Duration of current playing media in seconds. """
|
||||
dur = self._trackinfo.get('duration', '0:00')
|
||||
|
||||
# If the speaker is playing from the "line-in" source, getting
|
||||
# track metadata can return NOT_IMPLEMENTED, which breaks the
|
||||
# volume logic below
|
||||
if dur == 'NOT_IMPLEMENTED':
|
||||
return None
|
||||
|
||||
return sum(60 ** x[0] * int(x[1]) for x in
|
||||
enumerate(reversed(dur.split(':'))))
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
""" Image url of current playing media. """
|
||||
if 'album_art' in self._trackinfo:
|
||||
return self._trackinfo['album_art']
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
""" Title of current playing media. """
|
||||
if 'artist' in self._trackinfo and 'title' in self._trackinfo:
|
||||
return '{artist} - {title}'.format(
|
||||
artist=self._trackinfo['artist'],
|
||||
title=self._trackinfo['title']
|
||||
)
|
||||
if 'title' in self._status:
|
||||
return self._trackinfo['title']
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
""" Flags of media commands that are supported. """
|
||||
return SUPPORT_SONOS
|
||||
|
||||
def turn_off(self):
|
||||
""" turn_off media player. """
|
||||
self._player.pause()
|
||||
|
||||
def volume_up(self):
|
||||
""" volume_up media player. """
|
||||
self._player.volume += 1
|
||||
|
||||
def volume_down(self):
|
||||
""" volume_down media player. """
|
||||
self._player.volume -= 1
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
""" set volume level, range 0..1. """
|
||||
self._player.volume = str(int(volume * 100))
|
||||
|
||||
def mute_volume(self, mute):
|
||||
""" mute (true) or unmute (false) media player. """
|
||||
self._player.mute = mute
|
||||
|
||||
def media_play(self):
|
||||
""" media_play media player. """
|
||||
self._player.play()
|
||||
|
||||
def media_pause(self):
|
||||
""" media_pause media player. """
|
||||
self._player.pause()
|
||||
|
||||
def media_next_track(self):
|
||||
""" Send next track command. """
|
||||
self._player.next()
|
||||
|
||||
def media_previous_track(self):
|
||||
""" Send next track command. """
|
||||
self._player.previous()
|
||||
|
||||
def media_seek(self, position):
|
||||
""" Send seek command. """
|
||||
self._player.seek(str(datetime.timedelta(seconds=int(position))))
|
||||
|
||||
def turn_on(self):
|
||||
""" turn the media player on. """
|
||||
self._player.play()
|
||||
@@ -23,6 +23,7 @@ mqtt:
|
||||
keepalive: 60
|
||||
username: your_username
|
||||
password: your_secret_password
|
||||
certificate: /home/paulus/dev/addtrustexternalcaroot.crt
|
||||
|
||||
Variables:
|
||||
|
||||
@@ -42,8 +43,13 @@ Default is a random generated one.
|
||||
keepalive
|
||||
*Optional
|
||||
The keep alive in seconds for this client. Default is 60.
|
||||
|
||||
certificate
|
||||
*Optional
|
||||
Certificate to use for encrypting the connection to the broker.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -60,6 +66,7 @@ MQTT_CLIENT = None
|
||||
|
||||
DEFAULT_PORT = 1883
|
||||
DEFAULT_KEEPALIVE = 60
|
||||
DEFAULT_QOS = 0
|
||||
|
||||
SERVICE_PUBLISH = 'publish'
|
||||
EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED'
|
||||
@@ -73,23 +80,25 @@ CONF_CLIENT_ID = 'client_id'
|
||||
CONF_KEEPALIVE = 'keepalive'
|
||||
CONF_USERNAME = 'username'
|
||||
CONF_PASSWORD = 'password'
|
||||
CONF_CERTIFICATE = 'certificate'
|
||||
|
||||
ATTR_TOPIC = 'topic'
|
||||
ATTR_PAYLOAD = 'payload'
|
||||
ATTR_QOS = 'qos'
|
||||
|
||||
|
||||
def publish(hass, topic, payload, qos=0):
|
||||
def publish(hass, topic, payload, qos=None):
|
||||
""" Send an MQTT message. """
|
||||
data = {
|
||||
ATTR_TOPIC: topic,
|
||||
ATTR_PAYLOAD: payload,
|
||||
ATTR_QOS: qos,
|
||||
}
|
||||
if qos is not None:
|
||||
data[ATTR_QOS] = qos
|
||||
hass.services.call(DOMAIN, SERVICE_PUBLISH, data)
|
||||
|
||||
|
||||
def subscribe(hass, topic, callback, qos=0):
|
||||
def subscribe(hass, topic, callback, qos=DEFAULT_QOS):
|
||||
""" Subscribe to a topic. """
|
||||
def mqtt_topic_subscriber(event):
|
||||
""" Match subscribed MQTT topic. """
|
||||
@@ -117,11 +126,18 @@ def setup(hass, config):
|
||||
keepalive = util.convert(conf.get(CONF_KEEPALIVE), int, DEFAULT_KEEPALIVE)
|
||||
username = util.convert(conf.get(CONF_USERNAME), str)
|
||||
password = util.convert(conf.get(CONF_PASSWORD), str)
|
||||
certificate = util.convert(conf.get(CONF_CERTIFICATE), str)
|
||||
|
||||
# For cloudmqtt.com, secured connection, auto fill in certificate
|
||||
if certificate is None and 19999 < port < 30000 and \
|
||||
broker.endswith('.cloudmqtt.com'):
|
||||
certificate = os.path.join(os.path.dirname(__file__),
|
||||
'addtrustexternalcaroot.crt')
|
||||
|
||||
global MQTT_CLIENT
|
||||
try:
|
||||
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, username,
|
||||
password)
|
||||
password, certificate)
|
||||
except socket.error:
|
||||
_LOGGER.exception("Can't connect to the broker. "
|
||||
"Please check your settings and the broker "
|
||||
@@ -141,7 +157,7 @@ def setup(hass, config):
|
||||
""" Handle MQTT publish service calls. """
|
||||
msg_topic = call.data.get(ATTR_TOPIC)
|
||||
payload = call.data.get(ATTR_PAYLOAD)
|
||||
qos = call.data.get(ATTR_QOS)
|
||||
qos = call.data.get(ATTR_QOS, DEFAULT_QOS)
|
||||
if msg_topic is None or payload is None:
|
||||
return
|
||||
MQTT_CLIENT.publish(msg_topic, payload, qos)
|
||||
@@ -159,7 +175,7 @@ def setup(hass, config):
|
||||
class MQTT(object): # pragma: no cover
|
||||
""" Implements messaging service for MQTT. """
|
||||
def __init__(self, hass, broker, port, client_id, keepalive, username,
|
||||
password):
|
||||
password, certificate):
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
self.hass = hass
|
||||
@@ -170,8 +186,12 @@ class MQTT(object): # pragma: no cover
|
||||
self._mqttc = mqtt.Client()
|
||||
else:
|
||||
self._mqttc = mqtt.Client(client_id)
|
||||
|
||||
if username is not None:
|
||||
self._mqttc.username_pw_set(username, password)
|
||||
if certificate is not None:
|
||||
self._mqttc.tls_set(certificate)
|
||||
|
||||
self._mqttc.on_subscribe = self._mqtt_on_subscribe
|
||||
self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe
|
||||
self._mqttc.on_connect = self._mqtt_on_connect
|
||||
@@ -207,6 +227,17 @@ class MQTT(object): # pragma: no cover
|
||||
|
||||
def _mqtt_on_connect(self, mqttc, obj, flags, result_code):
|
||||
""" On connect, resubscribe to all topics we were subscribed to. """
|
||||
if result_code != 0:
|
||||
_LOGGER.error('Unable to connect to the MQTT broker: %s', {
|
||||
1: 'Incorrect protocol version',
|
||||
2: 'Invalid client identifier',
|
||||
3: 'Server unavailable',
|
||||
4: 'Bad username or password',
|
||||
5: 'Not authorised'
|
||||
}.get(result_code))
|
||||
self._mqttc.disconnect()
|
||||
return
|
||||
|
||||
old_topics = self.topics
|
||||
self._progress = {}
|
||||
self.topics = {}
|
||||
@@ -0,0 +1,25 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU
|
||||
MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs
|
||||
IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290
|
||||
MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux
|
||||
FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h
|
||||
bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v
|
||||
dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt
|
||||
H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9
|
||||
uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX
|
||||
mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX
|
||||
a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN
|
||||
E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0
|
||||
WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD
|
||||
VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0
|
||||
Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU
|
||||
cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx
|
||||
IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN
|
||||
AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH
|
||||
YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5
|
||||
6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC
|
||||
Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX
|
||||
c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a
|
||||
mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -6,7 +6,9 @@ Provides functionality to notify people.
|
||||
"""
|
||||
from functools import partial
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers import config_per_platform
|
||||
|
||||
@@ -36,6 +38,9 @@ def setup(hass, config):
|
||||
""" Sets up notify services. """
|
||||
success = False
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
for platform, p_config in config_per_platform(config, DOMAIN, _LOGGER):
|
||||
# get platform
|
||||
notify_implementation = get_component(
|
||||
@@ -69,7 +74,8 @@ def setup(hass, config):
|
||||
# register service
|
||||
service_call_handler = partial(notify_message, notify_service)
|
||||
service_notify = p_config.get(CONF_NAME, SERVICE_NOTIFY)
|
||||
hass.services.register(DOMAIN, service_notify, service_call_handler)
|
||||
hass.services.register(DOMAIN, service_notify, service_call_handler,
|
||||
descriptions.get(service_notify))
|
||||
success = True
|
||||
|
||||
return success
|
||||
|
||||
@@ -140,13 +140,19 @@ class MailNotificationService(BaseNotificationService):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.recipient = recipient
|
||||
self.tries = 2
|
||||
self.mail = None
|
||||
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
""" Connect/Authenticate to SMTP Server """
|
||||
|
||||
self.mail = smtplib.SMTP(self._server, self._port)
|
||||
self.mail.ehlo_or_helo_if_needed()
|
||||
if self.starttls == 1:
|
||||
self.mail.starttls()
|
||||
self.mail.ehlo()
|
||||
|
||||
self.mail.login(self.username, self.password)
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
@@ -160,4 +166,12 @@ class MailNotificationService(BaseNotificationService):
|
||||
msg['From'] = self._sender
|
||||
msg['X-Mailer'] = 'HomeAssistant'
|
||||
|
||||
self.mail.sendmail(self._sender, self.recipient, msg.as_string())
|
||||
for _ in range(self.tries):
|
||||
try:
|
||||
self.mail.sendmail(self._sender, self.recipient,
|
||||
msg.as_string())
|
||||
break
|
||||
except smtplib.SMTPException:
|
||||
_LOGGER.warning('SMTPException sending mail: '
|
||||
'retrying connection')
|
||||
self.connect()
|
||||
|
||||
@@ -256,7 +256,7 @@ class Recorder(threading.Thread):
|
||||
""" Query the database. """
|
||||
try:
|
||||
with self.conn, self.lock:
|
||||
_LOGGER.info("Running query %s", sql_query)
|
||||
_LOGGER.debug("Running query %s", sql_query)
|
||||
|
||||
cur = self.conn.cursor()
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ ATTR_ACTIVE_REQUESTED = "active_requested"
|
||||
|
||||
CONF_ENTITIES = "entities"
|
||||
|
||||
SceneConfig = namedtuple('SceneConfig', ['name', 'states'])
|
||||
SceneConfig = namedtuple('SceneConfig', ['name', 'states', 'fuzzy_match'])
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@@ -71,6 +71,15 @@ def setup(hass, config):
|
||||
def _process_config(scene_config):
|
||||
""" Process passed in config into a format to work with. """
|
||||
name = scene_config.get('name')
|
||||
|
||||
fuzzy_match = scene_config.get('fuzzy_match')
|
||||
if fuzzy_match:
|
||||
# default to 1%
|
||||
if isinstance(fuzzy_match, int):
|
||||
fuzzy_match /= 100.0
|
||||
else:
|
||||
fuzzy_match = 0.01
|
||||
|
||||
states = {}
|
||||
c_entities = dict(scene_config.get(CONF_ENTITIES, {}))
|
||||
|
||||
@@ -91,7 +100,7 @@ def _process_config(scene_config):
|
||||
|
||||
states[entity_id.lower()] = State(entity_id, state, attributes)
|
||||
|
||||
return SceneConfig(name, states)
|
||||
return SceneConfig(name, states, fuzzy_match)
|
||||
|
||||
|
||||
class Scene(ToggleEntity):
|
||||
@@ -179,9 +188,31 @@ class Scene(ToggleEntity):
|
||||
state = self.scene_config.states.get(cur_state and cur_state.entity_id)
|
||||
|
||||
return (cur_state is not None and state.state == cur_state.state and
|
||||
all(value == cur_state.attributes.get(key)
|
||||
all(self._compare_state_attribites(
|
||||
value, cur_state.attributes.get(key))
|
||||
for key, value in state.attributes.items()))
|
||||
|
||||
def _fuzzy_attribute_compare(self, attr_a, attr_b):
|
||||
"""
|
||||
Compare the attributes passed, use fuzzy logic if they are floats.
|
||||
"""
|
||||
|
||||
if not (isinstance(attr_a, float) and isinstance(attr_b, float)):
|
||||
return False
|
||||
diff = abs(attr_a - attr_b) / (abs(attr_a) + abs(attr_b))
|
||||
return diff <= self.scene_config.fuzzy_match
|
||||
|
||||
def _compare_state_attribites(self, attr1, attr2):
|
||||
""" Compare the attributes passed, using fuzzy logic if specified. """
|
||||
if attr1 == attr2:
|
||||
return True
|
||||
if not self.scene_config.fuzzy_match:
|
||||
return False
|
||||
if isinstance(attr1, list):
|
||||
return all(self._fuzzy_attribute_compare(a, b)
|
||||
for a, b in zip(attr1, attr2))
|
||||
return self._fuzzy_attribute_compare(attr1, attr2)
|
||||
|
||||
def _reproduce_state(self, states):
|
||||
""" Wraps reproduce state with Scence specific logic. """
|
||||
self.ignore_updates = True
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
"""
|
||||
homeassistant.components.scheduler
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
A component that will act as a scheduler and perform actions based
|
||||
on the events in the schedule.
|
||||
|
||||
It will read a json object from schedule.json in the config dir
|
||||
and create a schedule based on it.
|
||||
Each schedule is a JSON with the keys id, name, description,
|
||||
entity_ids, and events.
|
||||
- days is an array with the weekday number (monday=0) that the schedule
|
||||
is active
|
||||
- entity_ids an array with entity ids that the events in the schedule should
|
||||
effect (can also be groups)
|
||||
- events is an array of objects that describe the different events that is
|
||||
supported. Read in the events descriptions for more information.
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
|
||||
DOMAIN = 'scheduler'
|
||||
|
||||
DEPENDENCIES = []
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_SCHEDULE_FILE = 'schedule.json'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Create the schedules. """
|
||||
|
||||
def setup_listener(schedule, event_data):
|
||||
""" Creates the event listener based on event_data. """
|
||||
event_type = event_data['type']
|
||||
component = event_type
|
||||
|
||||
# if the event isn't part of a component
|
||||
if event_type in ['time']:
|
||||
component = 'scheduler.{}'.format(event_type)
|
||||
|
||||
elif not bootstrap.setup_component(hass, component, config):
|
||||
_LOGGER.warn("Could setup event listener for %s", component)
|
||||
return None
|
||||
|
||||
return get_component(component).create_event_listener(schedule,
|
||||
event_data)
|
||||
|
||||
def setup_schedule(schedule_data):
|
||||
""" Setup a schedule based on the description. """
|
||||
|
||||
schedule = Schedule(schedule_data['id'],
|
||||
name=schedule_data['name'],
|
||||
description=schedule_data['description'],
|
||||
entity_ids=schedule_data['entity_ids'],
|
||||
days=schedule_data['days'])
|
||||
|
||||
for event_data in schedule_data['events']:
|
||||
event_listener = setup_listener(schedule, event_data)
|
||||
|
||||
if event_listener:
|
||||
schedule.add_event_listener(event_listener)
|
||||
|
||||
schedule.schedule(hass)
|
||||
return True
|
||||
|
||||
with open(hass.config.path(_SCHEDULE_FILE)) as schedule_file:
|
||||
schedule_descriptions = json.load(schedule_file)
|
||||
|
||||
for schedule_description in schedule_descriptions:
|
||||
if not setup_schedule(schedule_description):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Schedule(object):
|
||||
""" A Schedule """
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, schedule_id, name=None, description=None,
|
||||
entity_ids=None, days=None):
|
||||
|
||||
self.schedule_id = schedule_id
|
||||
self.name = name
|
||||
self.description = description
|
||||
|
||||
self.entity_ids = entity_ids or []
|
||||
|
||||
self.days = days or [0, 1, 2, 3, 4, 5, 6]
|
||||
|
||||
self.__event_listeners = []
|
||||
|
||||
def add_event_listener(self, event_listener):
|
||||
""" Add a event to the schedule. """
|
||||
self.__event_listeners.append(event_listener)
|
||||
|
||||
def schedule(self, hass):
|
||||
""" Schedule all the events in the schedule. """
|
||||
for event in self.__event_listeners:
|
||||
event.schedule(hass)
|
||||
|
||||
|
||||
class EventListener(object):
|
||||
""" The base EventListener class that the schedule uses. """
|
||||
def __init__(self, schedule):
|
||||
self.my_schedule = schedule
|
||||
|
||||
def schedule(self, hass):
|
||||
""" Schedule the event """
|
||||
pass
|
||||
|
||||
def execute(self, hass):
|
||||
""" execute the event """
|
||||
pass
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class ServiceEventListener(EventListener):
|
||||
""" A EventListener that calls a service when executed. """
|
||||
|
||||
def __init__(self, schdule, service):
|
||||
EventListener.__init__(self, schdule)
|
||||
|
||||
(self.domain, self.service) = service.split('.')
|
||||
|
||||
def execute(self, hass):
|
||||
""" Call the service. """
|
||||
data = {ATTR_ENTITY_ID: self.my_schedule.entity_ids}
|
||||
hass.services.call(self.domain, self.service, data)
|
||||
|
||||
# Reschedule for next day
|
||||
self.schedule(hass)
|
||||
@@ -1,70 +0,0 @@
|
||||
"""
|
||||
homeassistant.components.scheduler.time
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
An event in the scheduler component that will call the service
|
||||
every specified day at the time specified.
|
||||
A time event need to have the type 'time', which service to call and at
|
||||
which time.
|
||||
|
||||
{
|
||||
"type": "time",
|
||||
"service": "switch.turn_off",
|
||||
"time": "22:00:00"
|
||||
}
|
||||
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
from homeassistant.components.scheduler import ServiceEventListener
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_event_listener(schedule, event_listener_data):
|
||||
""" Create a TimeEvent based on the description. """
|
||||
|
||||
service = event_listener_data['service']
|
||||
(hour, minute, second) = [int(x) for x in
|
||||
event_listener_data['time'].split(':')]
|
||||
|
||||
return TimeEventListener(schedule, service, hour, minute, second)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class TimeEventListener(ServiceEventListener):
|
||||
""" The time event that the scheduler uses. """
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, schedule, service, hour, minute, second):
|
||||
ServiceEventListener.__init__(self, schedule, service)
|
||||
|
||||
self.hour = hour
|
||||
self.minute = minute
|
||||
self.second = second
|
||||
|
||||
def schedule(self, hass):
|
||||
""" Schedule this event so that it will be called. """
|
||||
|
||||
next_time = dt_util.now().replace(
|
||||
hour=self.hour, minute=self.minute, second=self.second)
|
||||
|
||||
# Calculate the next time the event should be executed.
|
||||
# That is the next day that the schedule is configured to run
|
||||
while next_time < dt_util.now() or \
|
||||
next_time.weekday() not in self.my_schedule.days:
|
||||
|
||||
next_time = next_time + timedelta(days=1)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def execute(now):
|
||||
""" Call the execute method """
|
||||
self.execute(hass)
|
||||
|
||||
track_point_in_time(hass, execute, next_time)
|
||||
|
||||
_LOGGER.info(
|
||||
'TimeEventListener scheduled for %s, will call service %s.%s',
|
||||
next_time, self.domain, self.service)
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
homeassistant.components.script
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
entity_id
|
||||
Scripts are a sequence of actions that can be triggered manually
|
||||
by the user or automatically based upon automation events, etc.
|
||||
"""
|
||||
@@ -22,7 +22,10 @@ CONF_ALIAS = "alias"
|
||||
CONF_SERVICE = "execute_service"
|
||||
CONF_SERVICE_DATA = "service_data"
|
||||
CONF_SEQUENCE = "sequence"
|
||||
CONF_EVENT = "event"
|
||||
CONF_EVENT_DATA = "event_data"
|
||||
CONF_DELAY = "delay"
|
||||
ATTR_ENTITY_ID = "entity_id"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,15 +44,22 @@ def setup(hass, config):
|
||||
hass.services.register(DOMAIN, name, script)
|
||||
scripts.append(script)
|
||||
|
||||
def _get_entities(service):
|
||||
""" Make sure that we always get a list of entities """
|
||||
if isinstance(service.data[ATTR_ENTITY_ID], list):
|
||||
return service.data[ATTR_ENTITY_ID]
|
||||
else:
|
||||
return [service.data[ATTR_ENTITY_ID]]
|
||||
|
||||
def turn_on(service):
|
||||
""" Calls a script. """
|
||||
for entity_id in service.data['entity_id']:
|
||||
for entity_id in _get_entities(service):
|
||||
domain, service = split_entity_id(entity_id)
|
||||
hass.services.call(domain, service, {})
|
||||
|
||||
def turn_off(service):
|
||||
""" Cancels a script. """
|
||||
for entity_id in service.data['entity_id']:
|
||||
for entity_id in _get_entities(service):
|
||||
for script in scripts:
|
||||
if script.entity_id == entity_id:
|
||||
script.cancel()
|
||||
@@ -109,6 +119,8 @@ class Script(object):
|
||||
for action in self.actions:
|
||||
if CONF_SERVICE in action:
|
||||
self._call_service(action)
|
||||
elif CONF_EVENT in action:
|
||||
self._fire_event(action)
|
||||
elif CONF_DELAY in action:
|
||||
delay = timedelta(**action[CONF_DELAY])
|
||||
point_in_time = date_util.now() + delay
|
||||
@@ -140,3 +152,10 @@ class Script(object):
|
||||
domain, service = split_entity_id(action[CONF_SERVICE])
|
||||
data = action.get(CONF_SERVICE_DATA, {})
|
||||
self.hass.services.call(domain, service, data)
|
||||
|
||||
def _fire_event(self, action):
|
||||
""" Fires an event. """
|
||||
self.last_action = action.get(CONF_ALIAS, action[CONF_EVENT])
|
||||
_LOGGER.info("Executing script %s step %s", self.alias,
|
||||
self.last_action)
|
||||
self.hass.bus.fire(action[CONF_EVENT], action.get(CONF_EVENT_DATA))
|
||||
|
||||
@@ -65,7 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
resource = config.get('resource', None)
|
||||
|
||||
try:
|
||||
response = get(resource)
|
||||
response = get(resource, timeout=10)
|
||||
except exceptions.MissingSchema:
|
||||
_LOGGER.error("Missing resource or schema in configuration. "
|
||||
"Add http:// to your URL.")
|
||||
@@ -141,7 +141,7 @@ class ArestData(object):
|
||||
def update(self):
|
||||
""" Gets the latest data from aREST device. """
|
||||
try:
|
||||
response = get(self.resource)
|
||||
response = get(self.resource, timeout=10)
|
||||
if 'error' in self.data:
|
||||
del self.data['error']
|
||||
self.data = response.json()['variables']
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
homeassistant.components.sensor.command_sensor
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Allows to configure custom shell commands to turn a value for a sensor.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the command_line sensor you will need to add something like the
|
||||
following to your configuration.yaml file.
|
||||
|
||||
sensor:
|
||||
platform: command_sensor
|
||||
name: "Command sensor"
|
||||
command: sensor_command
|
||||
unit_of_measurement: "°C"
|
||||
correction_factor: 0.0001
|
||||
decimal_places: 0
|
||||
|
||||
Variables:
|
||||
|
||||
name
|
||||
*Optional
|
||||
Name of the command sensor.
|
||||
|
||||
command
|
||||
*Required
|
||||
The action to take to get the value.
|
||||
|
||||
unit_of_measurement
|
||||
*Optional
|
||||
Defines the units of measurement of the sensor, if any.
|
||||
|
||||
correction_factor
|
||||
*Optional
|
||||
A float value to do some basic calculations.
|
||||
|
||||
decimal_places
|
||||
*Optional
|
||||
Number of decimal places of the value. Default is 0.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.command_sensor.html
|
||||
"""
|
||||
import logging
|
||||
import subprocess
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "Command Sensor"
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Add the Command Sensor. """
|
||||
|
||||
if config.get('command') is None:
|
||||
_LOGGER.error('Missing required variable: "command"')
|
||||
return False
|
||||
|
||||
data = CommandSensorData(config.get('command'))
|
||||
|
||||
add_devices_callback([CommandSensor(
|
||||
data,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get('unit_of_measurement'),
|
||||
config.get('correction_factor', 1.0),
|
||||
config.get('decimal_places', 0)
|
||||
)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
class CommandSensor(Entity):
|
||||
""" Represents a sensor that is returning a value of a shell commands. """
|
||||
def __init__(self, data, name, unit_of_measurement, corr_factor,
|
||||
decimal_places):
|
||||
self.data = data
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self._corr_factor = float(corr_factor)
|
||||
self._decimal_places = decimal_places
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the sensor. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
""" Unit the value is expressed in. """
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data and updates the state. """
|
||||
self.data.update()
|
||||
value = self.data.value
|
||||
|
||||
try:
|
||||
if value is not None:
|
||||
if self._corr_factor is not None:
|
||||
self._state = round((float(value) * self._corr_factor),
|
||||
self._decimal_places)
|
||||
else:
|
||||
self._state = value
|
||||
except ValueError:
|
||||
self._state = value
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class CommandSensorData(object):
|
||||
""" Class for handling the data retrieval. """
|
||||
|
||||
def __init__(self, command):
|
||||
self.command = command
|
||||
self.value = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data with a shell command. """
|
||||
_LOGGER.info('Running command: %s', self.command)
|
||||
|
||||
try:
|
||||
return_value = subprocess.check_output(self.command.split())
|
||||
self.value = return_value.strip().decode('utf-8')
|
||||
except subprocess.CalledProcessError:
|
||||
_LOGGER.error('Command failed: %s', self.command)
|
||||
@@ -0,0 +1,204 @@
|
||||
"""
|
||||
homeassistant.components.sensor.glances
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Gathers system information of hosts which running glances.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the glances sensor you will need to add something like the following
|
||||
to your configuration.yaml file.
|
||||
|
||||
sensor:
|
||||
platform: glances
|
||||
name: Glances sensor
|
||||
host: IP_ADDRESS
|
||||
port: 61208
|
||||
resources:
|
||||
- 'disk_use_percent'
|
||||
- 'disk_use'
|
||||
- 'disk_free'
|
||||
- 'memory_use_percent'
|
||||
- 'memory_use'
|
||||
- 'memory_free'
|
||||
- 'swap_use_percent'
|
||||
- 'swap_use'
|
||||
- 'swap_free'
|
||||
- 'processor_load'
|
||||
- 'process_running'
|
||||
- 'process_total'
|
||||
- 'process_thread'
|
||||
- 'process_sleeping'
|
||||
|
||||
Variables:
|
||||
|
||||
name
|
||||
*Optional
|
||||
The name of the sensor. Default is 'Glances Sensor'.
|
||||
|
||||
host
|
||||
*Required
|
||||
The IP address of your host, e.g. 192.168.1.32.
|
||||
|
||||
port
|
||||
*Optional
|
||||
The network port to connect to. Default is 61208.
|
||||
|
||||
resources
|
||||
*Required
|
||||
Resources to monitor on the host. See the configuration example above for a
|
||||
list of all available conditions to monitor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.glances.html
|
||||
"""
|
||||
import logging
|
||||
import requests
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Glances Sensor'
|
||||
_RESOURCE = '/api/2/all'
|
||||
SENSOR_TYPES = {
|
||||
'disk_use_percent': ['Disk Use', '%'],
|
||||
'disk_use': ['Disk Use', 'GiB'],
|
||||
'disk_free': ['Disk Free', 'GiB'],
|
||||
'memory_use_percent': ['RAM Use', '%'],
|
||||
'memory_use': ['RAM Use', 'MiB'],
|
||||
'memory_free': ['RAM Free', 'MiB'],
|
||||
'swap_use_percent': ['Swap Use', '%'],
|
||||
'swap_use': ['Swap Use', 'GiB'],
|
||||
'swap_free': ['Swap Free', 'GiB'],
|
||||
'processor_load': ['CPU Load', ''],
|
||||
'process_running': ['Running', ''],
|
||||
'process_total': ['Total', ''],
|
||||
'process_thread': ['Thread', ''],
|
||||
'process_sleeping': ['Sleeping', '']
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Setup the Glances sensor. """
|
||||
|
||||
if not config.get('host'):
|
||||
_LOGGER.error('"host:" is missing your configuration')
|
||||
return False
|
||||
|
||||
host = config.get('host')
|
||||
port = config.get('port', 61208)
|
||||
url = 'http://{}:{}{}'.format(host, port, _RESOURCE)
|
||||
|
||||
try:
|
||||
response = requests.get(url, timeout=10)
|
||||
if not response.ok:
|
||||
_LOGGER.error('Response status is "%s"', response.status_code)
|
||||
return False
|
||||
except requests.exceptions.MissingSchema:
|
||||
_LOGGER.error('Missing resource or schema in configuration. '
|
||||
'Please heck our details in the configuration file.')
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error('No route to resource/endpoint. '
|
||||
'Please check the details in the configuration file.')
|
||||
return False
|
||||
|
||||
rest = GlancesData(url)
|
||||
|
||||
dev = []
|
||||
for resource in config['resources']:
|
||||
if resource not in SENSOR_TYPES:
|
||||
_LOGGER.error('Sensor type: "%s" does not exist', resource)
|
||||
else:
|
||||
dev.append(GlancesSensor(rest, resource))
|
||||
|
||||
add_devices(dev)
|
||||
|
||||
|
||||
class GlancesSensor(Entity):
|
||||
""" Implements a REST sensor. """
|
||||
|
||||
def __init__(self, rest, sensor_type):
|
||||
self.rest = rest
|
||||
self._name = SENSOR_TYPES[sensor_type][0]
|
||||
self.type = sensor_type
|
||||
self._state = None
|
||||
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the sensor. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
""" Unit the value is expressed in. """
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def update(self):
|
||||
""" Gets the latest data from REST API and updates the state. """
|
||||
self.rest.update()
|
||||
value = self.rest.data
|
||||
|
||||
if value is not None:
|
||||
if self.type == 'disk_use_percent':
|
||||
self._state = value['fs'][0]['percent']
|
||||
elif self.type == 'disk_use':
|
||||
self._state = round(value['fs'][0]['used'] / 1024**3, 1)
|
||||
elif self.type == 'disk_free':
|
||||
self._state = round(value['fs'][0]['free'] / 1024**3, 1)
|
||||
elif self.type == 'memory_use_percent':
|
||||
self._state = value['mem']['percent']
|
||||
elif self.type == 'memory_use':
|
||||
self._state = round(value['mem']['used'] / 1024**2, 1)
|
||||
elif self.type == 'memory_free':
|
||||
self._state = round(value['mem']['free'] / 1024**2, 1)
|
||||
elif self.type == 'swap_use_percent':
|
||||
self._state = value['memswap']['percent']
|
||||
elif self.type == 'swap_use':
|
||||
self._state = round(value['memswap']['used'] / 1024**3, 1)
|
||||
elif self.type == 'swap_free':
|
||||
self._state = round(value['memswap']['free'] / 1024**3, 1)
|
||||
elif self.type == 'processor_load':
|
||||
self._state = value['load']['min15']
|
||||
elif self.type == 'process_running':
|
||||
self._state = value['processcount']['running']
|
||||
elif self.type == 'process_total':
|
||||
self._state = value['processcount']['total']
|
||||
elif self.type == 'process_thread':
|
||||
self._state = value['processcount']['thread']
|
||||
elif self.type == 'process_sleeping':
|
||||
self._state = value['processcount']['sleeping']
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class GlancesData(object):
|
||||
""" Class for handling the data retrieval. """
|
||||
|
||||
def __init__(self, resource):
|
||||
self.resource = resource
|
||||
self.data = dict()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data from REST service. """
|
||||
try:
|
||||
response = requests.get(self.resource, timeout=10)
|
||||
self.data = response.json()
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to host/endpoint.")
|
||||
self.data = None
|
||||
@@ -91,7 +91,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.error(
|
||||
"Connection error "
|
||||
"Please check your settings for OpenWeatherMap.")
|
||||
return None
|
||||
return False
|
||||
|
||||
data = WeatherData(owm, forecast, hass.config.latitude,
|
||||
hass.config.longitude)
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
homeassistant.components.sensor.rest
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The rest sensor will consume JSON responses sent by an exposed REST API.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.rest.html
|
||||
"""
|
||||
import logging
|
||||
import requests
|
||||
from json import loads
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'REST Sensor'
|
||||
DEFAULT_METHOD = 'GET'
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the REST sensor. """
|
||||
|
||||
use_get = False
|
||||
use_post = False
|
||||
|
||||
resource = config.get('resource', None)
|
||||
method = config.get('method', DEFAULT_METHOD)
|
||||
payload = config.get('payload', None)
|
||||
verify_ssl = config.get('verify_ssl', True)
|
||||
|
||||
if method == 'GET':
|
||||
use_get = True
|
||||
elif method == 'POST':
|
||||
use_post = True
|
||||
|
||||
try:
|
||||
if use_get:
|
||||
response = requests.get(resource, timeout=10, verify=verify_ssl)
|
||||
elif use_post:
|
||||
response = requests.post(resource, data=payload, timeout=10,
|
||||
verify=verify_ssl)
|
||||
if not response.ok:
|
||||
_LOGGER.error('Response status is "%s"', response.status_code)
|
||||
return False
|
||||
except requests.exceptions.MissingSchema:
|
||||
_LOGGER.error('Missing resource or schema in configuration. '
|
||||
'Add http:// to your URL.')
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error('No route to resource/endpoint. '
|
||||
'Please check the URL in the configuration file.')
|
||||
return False
|
||||
|
||||
try:
|
||||
data = loads(response.text)
|
||||
except ValueError:
|
||||
_LOGGER.error('No valid JSON in the response in: %s', data)
|
||||
return False
|
||||
|
||||
try:
|
||||
RestSensor.extract_value(data, config.get('variable'))
|
||||
except KeyError:
|
||||
_LOGGER.error('Variable "%s" not found in response: "%s"',
|
||||
config.get('variable'), data)
|
||||
return False
|
||||
|
||||
if use_get:
|
||||
rest = RestDataGet(resource, verify_ssl)
|
||||
elif use_post:
|
||||
rest = RestDataPost(resource, payload, verify_ssl)
|
||||
|
||||
add_devices([RestSensor(rest,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get('variable'),
|
||||
config.get('unit_of_measurement'),
|
||||
config.get('correction_factor', None),
|
||||
config.get('decimal_places', None))])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
class RestSensor(Entity):
|
||||
""" Implements a REST sensor. """
|
||||
|
||||
def __init__(self, rest, name, variable, unit_of_measurement, corr_factor,
|
||||
decimal_places):
|
||||
self.rest = rest
|
||||
self._name = name
|
||||
self._variable = variable
|
||||
self._state = 'n/a'
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self._corr_factor = corr_factor
|
||||
self._decimal_places = decimal_places
|
||||
self.update()
|
||||
|
||||
@classmethod
|
||||
def extract_value(cls, data, variable):
|
||||
""" Extracts the value using a key name or a path. """
|
||||
if isinstance(variable, list):
|
||||
for variable_item in variable:
|
||||
data = data[variable_item]
|
||||
return data
|
||||
else:
|
||||
return data[variable]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the sensor. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
""" Unit the value is expressed in. """
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data from REST API and updates the state. """
|
||||
self.rest.update()
|
||||
value = self.rest.data
|
||||
|
||||
if 'error' in value:
|
||||
self._state = value['error']
|
||||
else:
|
||||
try:
|
||||
if value is not None:
|
||||
value = RestSensor.extract_value(value, self._variable)
|
||||
if self._corr_factor is not None \
|
||||
and self._decimal_places is not None:
|
||||
self._state = round(
|
||||
(float(value) *
|
||||
float(self._corr_factor)),
|
||||
self._decimal_places)
|
||||
elif self._corr_factor is not None \
|
||||
and self._decimal_places is None:
|
||||
self._state = round(float(value) *
|
||||
float(self._corr_factor))
|
||||
else:
|
||||
self._state = value
|
||||
except ValueError:
|
||||
self._state = RestSensor.extract_value(value, self._variable)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class RestDataGet(object):
|
||||
""" Class for handling the data retrieval with GET method. """
|
||||
|
||||
def __init__(self, resource, verify_ssl):
|
||||
self._resource = resource
|
||||
self._verify_ssl = verify_ssl
|
||||
self.data = dict()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data from REST service with GET method. """
|
||||
try:
|
||||
response = requests.get(self._resource, timeout=10,
|
||||
verify=self._verify_ssl)
|
||||
if 'error' in self.data:
|
||||
del self.data['error']
|
||||
self.data = response.json()
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to resource/endpoint.")
|
||||
self.data['error'] = 'N/A'
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class RestDataPost(object):
|
||||
""" Class for handling the data retrieval with POST method. """
|
||||
|
||||
def __init__(self, resource, payload, verify_ssl):
|
||||
self._resource = resource
|
||||
self._payload = payload
|
||||
self._verify_ssl = verify_ssl
|
||||
self.data = dict()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data from REST service with POST method. """
|
||||
try:
|
||||
response = requests.post(self._resource, data=self._payload,
|
||||
timeout=10, verify=self._verify_ssl)
|
||||
if 'error' in self.data:
|
||||
del self.data['error']
|
||||
self.data = response.json()
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to resource/endpoint.")
|
||||
self.data['error'] = 'N/A'
|
||||
@@ -3,7 +3,8 @@
|
||||
homeassistant.components.sensor.rpi_gpio
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Allows to configure a binary state sensor using RPi GPIO.
|
||||
Note: To use RPi GPIO, Home Assistant must be run as root.
|
||||
To avoid having to run Home Assistant as root when using this component,
|
||||
run a Raspbian version released at or after September 29, 2015.
|
||||
|
||||
sensor:
|
||||
platform: rpi_gpio
|
||||
|
||||
@@ -31,7 +31,7 @@ Details for the API : http://transport.opendata.ch
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from requests import get
|
||||
import requests
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.util.dt as dt_util
|
||||
@@ -53,7 +53,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
try:
|
||||
for location in [config.get('from', None), config.get('to', None)]:
|
||||
# transport.opendata.ch doesn't play nice with requests.Session
|
||||
result = get(_RESOURCE + 'locations?query=%s' % location)
|
||||
result = requests.get(_RESOURCE + 'locations?query=%s' % location,
|
||||
timeout=10)
|
||||
journey.append(result.json()['stations'][0]['name'])
|
||||
except KeyError:
|
||||
_LOGGER.exception(
|
||||
@@ -109,14 +110,14 @@ class PublicTransportData(object):
|
||||
def update(self):
|
||||
""" Gets the latest data from opendata.ch. """
|
||||
|
||||
response = get(
|
||||
response = requests.get(
|
||||
_RESOURCE +
|
||||
'connections?' +
|
||||
'from=' + self.start + '&' +
|
||||
'to=' + self.destination + '&' +
|
||||
'fields[]=connections/from/departureTimestamp/&' +
|
||||
'fields[]=connections/')
|
||||
|
||||
'fields[]=connections/',
|
||||
timeout=10)
|
||||
connections = response.json()['connections'][:2]
|
||||
|
||||
try:
|
||||
|
||||
@@ -59,7 +59,6 @@ arg
|
||||
Additional details for the type, eg. path, binary name, etc.
|
||||
"""
|
||||
import logging
|
||||
import psutil
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -120,7 +119,7 @@ class SystemMonitorSensor(Entity):
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
return self._name.rstrip()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@@ -133,6 +132,7 @@ class SystemMonitorSensor(Entity):
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def update(self):
|
||||
import psutil
|
||||
if self.type == 'disk_use_percent':
|
||||
self._state = psutil.disk_usage(self.argument).percent
|
||||
elif self.type == 'disk_use':
|
||||
|
||||
@@ -34,7 +34,7 @@ import homeassistant.util as util
|
||||
|
||||
DatatypeDescription = namedtuple("DatatypeDescription", ['name', 'unit'])
|
||||
|
||||
REQUIREMENTS = ['tellcore-py==1.0.4']
|
||||
REQUIREMENTS = ['tellcore-py==1.1.2']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
@@ -36,12 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
hasattr(value, 'humidity') and value.humidity
|
||||
])
|
||||
|
||||
sensors.extend([
|
||||
VerisureAlarm(value)
|
||||
for value in verisure.get_alarm_status().values()
|
||||
if verisure.SHOW_ALARM
|
||||
])
|
||||
|
||||
add_devices(sensors)
|
||||
|
||||
|
||||
@@ -103,25 +97,3 @@ class VerisureHygrometer(Entity):
|
||||
def update(self):
|
||||
''' update sensor '''
|
||||
verisure.update()
|
||||
|
||||
|
||||
class VerisureAlarm(Entity):
|
||||
""" represents a Verisure alarm status within home assistant. """
|
||||
|
||||
def __init__(self, alarm_status):
|
||||
self._id = alarm_status.id
|
||||
self._device = verisure.MY_PAGES.DEVICE_ALARM
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return 'Alarm {}'.format(self._id)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return verisure.STATUS[self._device][self._id].label
|
||||
|
||||
def update(self):
|
||||
''' update sensor '''
|
||||
verisure.update()
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
homeassistant.components.sensor.worldclock
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The Worldclock sensor let you display the current time of a different time
|
||||
zone.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the Worldclock sensor you will need to add something like the
|
||||
following to your configuration.yaml file.
|
||||
|
||||
sensor:
|
||||
platform: worldclock
|
||||
time_zone: America/New_York
|
||||
name: New York
|
||||
|
||||
Variables:
|
||||
|
||||
time_zone
|
||||
*Required
|
||||
Time zone you want to display.
|
||||
|
||||
name
|
||||
*Optional
|
||||
Name of the sensor to use in the frontend.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.worldclock.html
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_NAME = "Worldclock Sensor"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the Worldclock sensor. """
|
||||
|
||||
try:
|
||||
time_zone = dt_util.get_time_zone(config.get('time_zone'))
|
||||
except AttributeError:
|
||||
_LOGGER.error("time_zone in platform configuration is missing.")
|
||||
return False
|
||||
|
||||
if time_zone is None:
|
||||
_LOGGER.error("Timezone '%s' is not valid.", config.get('time_zone'))
|
||||
return False
|
||||
|
||||
add_devices([WorldClockSensor(
|
||||
time_zone,
|
||||
config.get('name', DEFAULT_NAME)
|
||||
)])
|
||||
|
||||
|
||||
class WorldClockSensor(Entity):
|
||||
""" Implements a Worldclock sensor. """
|
||||
|
||||
def __init__(self, time_zone, name):
|
||||
self._name = name
|
||||
self._time_zone = time_zone
|
||||
self._state = None
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
""" Gets the time and updates the states. """
|
||||
self._state = dt_util.datetime_to_time_str(
|
||||
dt_util.now(time_zone=self._time_zone))
|
||||
@@ -25,10 +25,8 @@ import urllib
|
||||
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import (
|
||||
track_point_in_utc_time, track_point_in_time)
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components.scheduler import ServiceEventListener
|
||||
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['astral==0.8.1']
|
||||
@@ -214,95 +212,3 @@ class Sun(Entity):
|
||||
track_point_in_utc_time(
|
||||
self.hass, self.point_in_time_listener,
|
||||
self.next_change + timedelta(seconds=1))
|
||||
|
||||
|
||||
def create_event_listener(schedule, event_listener_data):
|
||||
""" Create a sun event listener based on the description. """
|
||||
|
||||
negative_offset = False
|
||||
service = event_listener_data['service']
|
||||
offset_str = event_listener_data['offset']
|
||||
event = event_listener_data['event']
|
||||
|
||||
if offset_str.startswith('-'):
|
||||
negative_offset = True
|
||||
offset_str = offset_str[1:]
|
||||
|
||||
(hour, minute, second) = [int(x) for x in offset_str.split(':')]
|
||||
|
||||
offset = timedelta(hours=hour, minutes=minute, seconds=second)
|
||||
|
||||
if event == 'sunset':
|
||||
return SunsetEventListener(schedule, service, offset, negative_offset)
|
||||
|
||||
return SunriseEventListener(schedule, service, offset, negative_offset)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class SunEventListener(ServiceEventListener):
|
||||
""" This is the base class for sun event listeners. """
|
||||
|
||||
def __init__(self, schedule, service, offset, negative_offset):
|
||||
ServiceEventListener.__init__(self, schedule, service)
|
||||
|
||||
self.offset = offset
|
||||
self.negative_offset = negative_offset
|
||||
|
||||
def __get_next_time(self, next_event):
|
||||
"""
|
||||
Returns when the next time the service should be called.
|
||||
Taking into account the offset and which days the event should execute.
|
||||
"""
|
||||
|
||||
if self.negative_offset:
|
||||
next_time = next_event - self.offset
|
||||
else:
|
||||
next_time = next_event + self.offset
|
||||
|
||||
while next_time < dt_util.now() or \
|
||||
next_time.weekday() not in self.my_schedule.days:
|
||||
next_time = next_time + timedelta(days=1)
|
||||
|
||||
return next_time
|
||||
|
||||
def schedule_next_event(self, hass, next_event):
|
||||
""" Schedule the event. """
|
||||
next_time = self.__get_next_time(next_event)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def execute(now):
|
||||
""" Call the execute method. """
|
||||
self.execute(hass)
|
||||
|
||||
track_point_in_time(hass, execute, next_time)
|
||||
|
||||
return next_time
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class SunsetEventListener(SunEventListener):
|
||||
""" This class is used the call a service when the sun sets. """
|
||||
def schedule(self, hass):
|
||||
""" Schedule the event """
|
||||
next_setting_dt = next_setting(hass)
|
||||
|
||||
next_time_dt = self.schedule_next_event(hass, next_setting_dt)
|
||||
|
||||
_LOGGER.info(
|
||||
'SunsetEventListener scheduled for %s, will call service %s.%s',
|
||||
next_time_dt, self.domain, self.service)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class SunriseEventListener(SunEventListener):
|
||||
""" This class is used the call a service when the sun rises. """
|
||||
|
||||
def schedule(self, hass):
|
||||
""" Schedule the event. """
|
||||
next_rising_dt = next_rising(hass)
|
||||
|
||||
next_time_dt = self.schedule_next_event(hass, next_rising_dt)
|
||||
|
||||
_LOGGER.info(
|
||||
'SunriseEventListener scheduled for %s, will call service %s.%s',
|
||||
next_time_dt, self.domain, self.service)
|
||||
|
||||
@@ -3,9 +3,11 @@ homeassistant.components.switch
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Component to interface with various switches that can be controlled remotely.
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
@@ -83,8 +85,12 @@ def setup(hass, config):
|
||||
if switch.should_poll:
|
||||
switch.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service)
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service)
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service,
|
||||
descriptions.get(SERVICE_TURN_OFF))
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service,
|
||||
descriptions.get(SERVICE_TURN_ON))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
homeassistant.components.switch.arduino
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for switching Arduino pins on and off. So fare only digital pins are
|
||||
Support for switching Arduino pins on and off. So far only digital pins are
|
||||
supported.
|
||||
|
||||
Configuration:
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
homeassistant.components.switch.arest
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The arest switch can control the digital pins of a device running with the
|
||||
aREST RESTful framework for Arduino, the ESP8266, and the Raspberry Pi.
|
||||
Only tested with Arduino boards so far.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the arest switch you will need to add something like the following
|
||||
to your configuration.yaml file.
|
||||
|
||||
sensor:
|
||||
platform: arest
|
||||
resource: http://IP_ADDRESS
|
||||
pins:
|
||||
11:
|
||||
name: Fan Office
|
||||
12:
|
||||
name: Light Desk
|
||||
|
||||
Variables:
|
||||
|
||||
resource:
|
||||
*Required
|
||||
IP address of the device that is exposing an aREST API.
|
||||
|
||||
pins:
|
||||
The number of the digital pin to switch.
|
||||
|
||||
These are the variables for the pins array:
|
||||
|
||||
name
|
||||
*Required
|
||||
The name for the pin that will be used in the frontend.
|
||||
|
||||
Details for the API: http://arest.io
|
||||
"""
|
||||
import logging
|
||||
from requests import get, exceptions
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.const import DEVICE_DEFAULT_NAME
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the aREST switches. """
|
||||
|
||||
resource = config.get('resource', None)
|
||||
|
||||
try:
|
||||
response = get(resource, timeout=10)
|
||||
except exceptions.MissingSchema:
|
||||
_LOGGER.error("Missing resource or schema in configuration. "
|
||||
"Add http:// to your URL.")
|
||||
return False
|
||||
except exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to device. "
|
||||
"Please check the IP address in the configuration file.")
|
||||
return False
|
||||
|
||||
dev = []
|
||||
pins = config.get('pins')
|
||||
for pinnum, pin in pins.items():
|
||||
dev.append(ArestSwitch(resource,
|
||||
response.json()['name'],
|
||||
pin.get('name'),
|
||||
pinnum))
|
||||
add_devices(dev)
|
||||
|
||||
|
||||
class ArestSwitch(SwitchDevice):
|
||||
""" Implements an aREST switch. """
|
||||
|
||||
def __init__(self, resource, location, name, pin):
|
||||
self._resource = resource
|
||||
self._name = '{} {}'.format(location.title(), name.title()) \
|
||||
or DEVICE_DEFAULT_NAME
|
||||
self._pin = pin
|
||||
self._state = None
|
||||
|
||||
request = get('{}/mode/{}/o'.format(self._resource, self._pin),
|
||||
timeout=10)
|
||||
if request.status_code is not 200:
|
||||
_LOGGER.error("Can't set mode. Is device offline?")
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the switch. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
request = get('{}/digital/{}/1'.format(self._resource, self._pin),
|
||||
timeout=10)
|
||||
if request.status_code == 200:
|
||||
self._state = True
|
||||
else:
|
||||
_LOGGER.error("Can't turn on pin %s at %s. Is device offline?",
|
||||
self._resource, self._pin)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
request = get('{}/digital/{}/0'.format(self._resource, self._pin),
|
||||
timeout=10)
|
||||
if request.status_code == 200:
|
||||
self._state = False
|
||||
else:
|
||||
_LOGGER.error("Can't turn off pin %s at %s. Is device offline?",
|
||||
self._resource, self._pin)
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data from aREST API and updates the state. """
|
||||
request = get('{}/digital/{}'.format(self._resource, self._pin),
|
||||
timeout=10)
|
||||
self._state = request.json()['return_value'] != 0
|
||||
@@ -2,13 +2,44 @@
|
||||
"""
|
||||
homeassistant.components.switch.command_switch
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Allows to configure custom shell commands to turn a switch on/off.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the command_line switch you will need to add something like the
|
||||
following to your configuration.yaml file.
|
||||
|
||||
switch:
|
||||
platform: command_switch
|
||||
switches:
|
||||
name_of_the_switch:
|
||||
oncmd: switch_command on for name_of_the_switch
|
||||
offcmd: switch_command off for name_of_the_switch
|
||||
|
||||
Variables:
|
||||
|
||||
These are the variables for the switches array:
|
||||
|
||||
name_of_the_switch
|
||||
*Required
|
||||
Name of the command switch. Multiple entries are possible.
|
||||
|
||||
oncmd
|
||||
*Required
|
||||
The action to take for on.
|
||||
|
||||
offcmd
|
||||
*Required
|
||||
The action to take for off.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.command_switch.html
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
import subprocess
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -22,7 +53,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
for dev_name, properties in switches.items():
|
||||
devices.append(
|
||||
CommandSwitch(
|
||||
dev_name,
|
||||
properties.get('name', dev_name),
|
||||
properties.get('oncmd', 'true'),
|
||||
properties.get('offcmd', 'true')))
|
||||
|
||||
|
||||
@@ -11,14 +11,14 @@ signal_repetitions: 3
|
||||
"""
|
||||
import logging
|
||||
|
||||
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP,
|
||||
ATTR_FRIENDLY_NAME)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
import tellcore.constants as tellcore_constants
|
||||
|
||||
from tellcore.library import DirectCallbackDispatcher
|
||||
SINGAL_REPETITIONS = 1
|
||||
|
||||
REQUIREMENTS = ['tellcore-py==1.0.4']
|
||||
REQUIREMENTS = ['tellcore-py==1.1.2']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@@ -31,16 +31,34 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"Failed to import tellcore")
|
||||
return
|
||||
|
||||
core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher())
|
||||
|
||||
signal_repetitions = config.get('signal_repetitions', SINGAL_REPETITIONS)
|
||||
|
||||
core = telldus.TelldusCore()
|
||||
switches_and_lights = core.devices()
|
||||
|
||||
switches = []
|
||||
|
||||
for switch in switches_and_lights:
|
||||
if not switch.methods(tellcore_constants.TELLSTICK_DIM):
|
||||
switches.append(TellstickSwitchDevice(switch, signal_repetitions))
|
||||
switches.append(
|
||||
TellstickSwitchDevice(switch, signal_repetitions))
|
||||
|
||||
def _device_event_callback(id_, method, data, cid):
|
||||
""" Called from the TelldusCore library to update one device """
|
||||
for switch_device in switches:
|
||||
if switch_device.tellstick_device.id == id_:
|
||||
switch_device.update_ha_state()
|
||||
break
|
||||
|
||||
callback_id = core.register_device_event(_device_event_callback)
|
||||
|
||||
def unload_telldus_lib(event):
|
||||
""" Un-register the callback bindings """
|
||||
if callback_id is not None:
|
||||
core.unregister_callback(callback_id)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, unload_telldus_lib)
|
||||
|
||||
add_devices_callback(switches)
|
||||
|
||||
@@ -50,15 +68,20 @@ class TellstickSwitchDevice(ToggleEntity):
|
||||
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
|
||||
tellcore_constants.TELLSTICK_TURNOFF)
|
||||
|
||||
def __init__(self, tellstick, signal_repetitions):
|
||||
self.tellstick = tellstick
|
||||
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
|
||||
def __init__(self, tellstick_device, signal_repetitions):
|
||||
self.tellstick_device = tellstick_device
|
||||
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name}
|
||||
self.signal_repetitions = signal_repetitions
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Tells Home Assistant not to poll this entity. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the switch if any. """
|
||||
return self.tellstick.name
|
||||
return self.tellstick_device.name
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
@@ -68,7 +91,7 @@ class TellstickSwitchDevice(ToggleEntity):
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if switch is on. """
|
||||
last_command = self.tellstick.last_sent_command(
|
||||
last_command = self.tellstick_device.last_sent_command(
|
||||
self.last_sent_command_mask)
|
||||
|
||||
return last_command == tellcore_constants.TELLSTICK_TURNON
|
||||
@@ -76,9 +99,11 @@ class TellstickSwitchDevice(ToggleEntity):
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
for _ in range(self.signal_repetitions):
|
||||
self.tellstick.turn_on()
|
||||
self.tellstick_device.turn_on()
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turns the switch off. """
|
||||
for _ in range(self.signal_repetitions):
|
||||
self.tellstick.turn_off()
|
||||
self.tellstick_device.turn_off()
|
||||
self.update_ha_state()
|
||||
|
||||
@@ -122,7 +122,7 @@ class VeraSwitch(ToggleEntity):
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
attr = super().state_attributes
|
||||
attr = super().state_attributes or {}
|
||||
|
||||
if self.vera_device.has_battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
||||
|
||||
@@ -9,7 +9,7 @@ import logging
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY
|
||||
|
||||
REQUIREMENTS = ['pywemo==0.3']
|
||||
REQUIREMENTS = ['pywemo==0.3.1']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@@ -123,9 +123,14 @@ class WemoSwitch(SwitchDevice):
|
||||
|
||||
def update(self):
|
||||
""" Update WeMo state. """
|
||||
self.wemo.get_state(True)
|
||||
if self.wemo.model_name == 'Insight':
|
||||
self.insight_params = self.wemo.insight_params
|
||||
self.insight_params['standby_state'] = self.wemo.get_standby_state
|
||||
elif self.wemo.model_name == 'Maker':
|
||||
self.maker_params = self.wemo.maker_params
|
||||
try:
|
||||
self.wemo.get_state(True)
|
||||
if self.wemo.model_name == 'Insight':
|
||||
self.insight_params = self.wemo.insight_params
|
||||
self.insight_params['standby_state'] = (
|
||||
self.wemo.get_standby_state)
|
||||
elif self.wemo.model_name == 'Maker':
|
||||
self.maker_params = self.wemo.maker_params
|
||||
except AttributeError:
|
||||
logging.getLogger(__name__).warning(
|
||||
'Could not update status for %s', self.name)
|
||||
|
||||
@@ -5,9 +5,11 @@ homeassistant.components.thermostat
|
||||
Provides functionality to interact with thermostats.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
import homeassistant.util as util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.temperature import convert
|
||||
@@ -23,10 +25,17 @@ SCAN_INTERVAL = 60
|
||||
SERVICE_SET_AWAY_MODE = "set_away_mode"
|
||||
SERVICE_SET_TEMPERATURE = "set_temperature"
|
||||
|
||||
STATE_HEAT = "heat"
|
||||
STATE_COOL = "cool"
|
||||
STATE_IDLE = "idle"
|
||||
|
||||
ATTR_CURRENT_TEMPERATURE = "current_temperature"
|
||||
ATTR_AWAY_MODE = "away_mode"
|
||||
ATTR_MAX_TEMP = "max_temp"
|
||||
ATTR_MIN_TEMP = "min_temp"
|
||||
ATTR_TEMPERATURE_LOW = "target_temp_low"
|
||||
ATTR_TEMPERATURE_HIGH = "target_temp_high"
|
||||
ATTR_OPERATION = "current_operation"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -94,11 +103,16 @@ def setup(hass, config):
|
||||
for thermostat in target_thermostats:
|
||||
thermostat.update_ha_state(True)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_AWAY_MODE, thermostat_service)
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_TEMPERATURE, thermostat_service)
|
||||
DOMAIN, SERVICE_SET_AWAY_MODE, thermostat_service,
|
||||
descriptions.get(SERVICE_SET_AWAY_MODE))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_TEMPERATURE, thermostat_service,
|
||||
descriptions.get(SERVICE_SET_TEMPERATURE))
|
||||
|
||||
return True
|
||||
|
||||
@@ -126,19 +140,25 @@ class ThermostatDevice(Entity):
|
||||
user_unit = self.hass.config.temperature_unit
|
||||
|
||||
data = {
|
||||
ATTR_CURRENT_TEMPERATURE: round(convert(self.current_temperature,
|
||||
thermostat_unit,
|
||||
user_unit), 1),
|
||||
ATTR_MIN_TEMP: round(convert(self.min_temp,
|
||||
thermostat_unit,
|
||||
user_unit), 0),
|
||||
ATTR_MAX_TEMP: round(convert(self.max_temp,
|
||||
thermostat_unit,
|
||||
user_unit), 0)
|
||||
ATTR_CURRENT_TEMPERATURE: round(convert(
|
||||
self.current_temperature, thermostat_unit, user_unit), 1),
|
||||
ATTR_MIN_TEMP: round(convert(
|
||||
self.min_temp, thermostat_unit, user_unit), 0),
|
||||
ATTR_MAX_TEMP: round(convert(
|
||||
self.max_temp, thermostat_unit, user_unit), 0),
|
||||
ATTR_TEMPERATURE: round(convert(
|
||||
self.target_temperature, thermostat_unit, user_unit), 0),
|
||||
ATTR_TEMPERATURE_LOW: round(convert(
|
||||
self.target_temperature_low, thermostat_unit, user_unit), 0),
|
||||
ATTR_TEMPERATURE_HIGH: round(convert(
|
||||
self.target_temperature_high, thermostat_unit, user_unit), 0),
|
||||
}
|
||||
|
||||
is_away = self.is_away_mode_on
|
||||
operation = self.operation
|
||||
if operation is not None:
|
||||
data[ATTR_OPERATION] = operation
|
||||
|
||||
is_away = self.is_away_mode_on
|
||||
if is_away is not None:
|
||||
data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF
|
||||
|
||||
@@ -152,18 +172,33 @@ class ThermostatDevice(Entity):
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
""" Unit of measurement this thermostat expresses itself in. """
|
||||
return NotImplementedError
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
""" Returns the current temperature. """
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def operation(self):
|
||||
""" Returns current operation ie. heat, cool, idle """
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
""" Returns the temperature we try to reach. """
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
""" Returns the lower bound temperature we try to reach. """
|
||||
return self.target_temperature
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
""" Returns the upper bound temperature we try to reach. """
|
||||
return self.target_temperature
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""
|
||||
|
||||
@@ -190,6 +190,13 @@ class HeatControl(ThermostatDevice):
|
||||
if self._heater_manual_changed:
|
||||
self.set_temperature(None)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""
|
||||
Returns if away mode is on.
|
||||
"""
|
||||
return self._away
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
""" Turns away mode on. """
|
||||
self._away = True
|
||||
|
||||
@@ -3,9 +3,11 @@ homeassistant.components.thermostat.nest
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Adds support for Nest thermostats.
|
||||
"""
|
||||
import socket
|
||||
import logging
|
||||
|
||||
from homeassistant.components.thermostat import ThermostatDevice
|
||||
from homeassistant.components.thermostat import (ThermostatDevice, STATE_COOL,
|
||||
STATE_IDLE, STATE_HEAT)
|
||||
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS)
|
||||
|
||||
REQUIREMENTS = ['python-nest==2.6.0']
|
||||
@@ -34,12 +36,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
napi = nest.Nest(username, password)
|
||||
|
||||
add_devices([
|
||||
NestThermostat(structure, device)
|
||||
for structure in napi.structures
|
||||
for device in structure.devices
|
||||
])
|
||||
try:
|
||||
add_devices([
|
||||
NestThermostat(structure, device)
|
||||
for structure in napi.structures
|
||||
for device in structure.devices
|
||||
])
|
||||
except socket.error:
|
||||
logger.error(
|
||||
"Connection error logging into the nest web service"
|
||||
)
|
||||
|
||||
|
||||
class NestThermostat(ThermostatDevice):
|
||||
@@ -83,25 +89,52 @@ class NestThermostat(ThermostatDevice):
|
||||
""" Returns the current temperature. """
|
||||
return round(self.device.temperature, 1)
|
||||
|
||||
@property
|
||||
def operation(self):
|
||||
""" Returns current operation ie. heat, cool, idle """
|
||||
if self.device.hvac_ac_state is True:
|
||||
return STATE_COOL
|
||||
elif self.device.hvac_heater_state is True:
|
||||
return STATE_HEAT
|
||||
else:
|
||||
return STATE_IDLE
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
""" Returns the temperature we try to reach. """
|
||||
target = self.device.target
|
||||
|
||||
if isinstance(target, tuple):
|
||||
if self.device.mode == 'range':
|
||||
low, high = target
|
||||
|
||||
if self.current_temperature < low:
|
||||
temp = low
|
||||
elif self.current_temperature > high:
|
||||
if self.operation == STATE_COOL:
|
||||
temp = high
|
||||
elif self.operation == STATE_HEAT:
|
||||
temp = low
|
||||
else:
|
||||
temp = (low + high)/2
|
||||
range_average = (low + high)/2
|
||||
if self.current_temperature < range_average:
|
||||
temp = low
|
||||
elif self.current_temperature >= range_average:
|
||||
temp = high
|
||||
else:
|
||||
temp = target
|
||||
|
||||
return round(temp, 1)
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
""" Returns the lower bound temperature we try to reach. """
|
||||
if self.device.mode == 'range':
|
||||
return round(self.device.target[0], 1)
|
||||
return round(self.target_temperature, 1)
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
""" Returns the upper bound temperature we try to reach. """
|
||||
if self.device.mode == 'range':
|
||||
return round(self.device.target[1], 1)
|
||||
return round(self.target_temperature, 1)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
""" Returns if away mode is on. """
|
||||
@@ -109,6 +142,11 @@ class NestThermostat(ThermostatDevice):
|
||||
|
||||
def set_temperature(self, temperature):
|
||||
""" Set new target temperature """
|
||||
if self.device.mode == 'range':
|
||||
if self.target_temperature == self.target_temperature_low:
|
||||
temperature = (temperature, self.target_temperature_high)
|
||||
elif self.target_temperature == self.target_temperature_high:
|
||||
temperature = (self.target_temperature_low, temperature)
|
||||
self.device.target = temperature
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
|
||||
@@ -59,8 +59,9 @@ from homeassistant.const import (
|
||||
DOMAIN = "verisure"
|
||||
DISCOVER_SENSORS = 'verisure.sensors'
|
||||
DISCOVER_SWITCHES = 'verisure.switches'
|
||||
DISCOVER_ALARMS = 'verisure.alarm_control_panel'
|
||||
|
||||
DEPENDENCIES = []
|
||||
DEPENDENCIES = ['alarm_control_panel']
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/persandstrom/python-verisure/archive/'
|
||||
'9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6'
|
||||
@@ -123,7 +124,8 @@ def setup(hass, config):
|
||||
|
||||
# Load components for the devices in the ISY controller that we support
|
||||
for comp_name, discovery in ((('sensor', DISCOVER_SENSORS),
|
||||
('switch', DISCOVER_SWITCHES))):
|
||||
('switch', DISCOVER_SWITCHES),
|
||||
('alarm_control_panel', DISCOVER_ALARMS))):
|
||||
component = get_component(comp_name)
|
||||
_LOGGER.info(config[DOMAIN])
|
||||
bootstrap.setup_component(hass, component.DOMAIN, config)
|
||||
@@ -166,7 +168,7 @@ def reconnect():
|
||||
def update():
|
||||
""" Updates the status of verisure components. """
|
||||
if WRONG_PASSWORD_GIVEN:
|
||||
# Is there any way to inform user?
|
||||
_LOGGER.error('Wrong password')
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
homeassistant.components.zone
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Allows defintion of zones in Home Assistant.
|
||||
|
||||
zone:
|
||||
name: School
|
||||
latitude: 32.8773367
|
||||
longitude: -117.2494053
|
||||
# Optional radius in meters (default: 100)
|
||||
radius: 250
|
||||
# Optional icon to show instead of name
|
||||
# See https://www.google.com/design/icons/
|
||||
# Example: home, work, group-work, shopping-cart, social:people
|
||||
icon: group-work
|
||||
|
||||
zone 2:
|
||||
name: Work
|
||||
latitude: 32.8753367
|
||||
longitude: -117.2474053
|
||||
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME)
|
||||
from homeassistant.helpers import extract_domain_configs, generate_entity_id
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util.location import distance
|
||||
|
||||
DOMAIN = "zone"
|
||||
DEPENDENCIES = []
|
||||
ENTITY_ID_FORMAT = 'zone.{}'
|
||||
ENTITY_ID_HOME = ENTITY_ID_FORMAT.format('home')
|
||||
STATE = 'zoning'
|
||||
|
||||
DEFAULT_NAME = 'Unnamed zone'
|
||||
|
||||
ATTR_RADIUS = 'radius'
|
||||
DEFAULT_RADIUS = 100
|
||||
|
||||
ATTR_ICON = 'icon'
|
||||
ICON_HOME = 'home'
|
||||
|
||||
|
||||
def active_zone(hass, latitude, longitude, radius=0):
|
||||
""" Find the active zone for given latitude, longitude. """
|
||||
# Sort entity IDs so that we are deterministic if equal distance to 2 zones
|
||||
zones = (hass.states.get(entity_id) for entity_id
|
||||
in sorted(hass.states.entity_ids(DOMAIN)))
|
||||
|
||||
min_dist = None
|
||||
closest = None
|
||||
|
||||
for zone in zones:
|
||||
zone_dist = distance(
|
||||
latitude, longitude,
|
||||
zone.attributes[ATTR_LATITUDE], zone.attributes[ATTR_LONGITUDE])
|
||||
|
||||
within_zone = zone_dist - radius < zone.attributes[ATTR_RADIUS]
|
||||
closer_zone = closest is None or zone_dist < min_dist
|
||||
smaller_zone = (zone_dist == min_dist and
|
||||
zone.attributes[ATTR_RADIUS] <
|
||||
closest.attributes[ATTR_RADIUS])
|
||||
|
||||
if within_zone and (closer_zone or smaller_zone):
|
||||
min_dist = zone_dist
|
||||
closest = zone
|
||||
|
||||
return closest
|
||||
|
||||
|
||||
def in_zone(zone, latitude, longitude, radius=0):
|
||||
""" Test if given latitude, longitude is in given zone. """
|
||||
zone_dist = distance(
|
||||
latitude, longitude,
|
||||
zone.attributes[ATTR_LATITUDE], zone.attributes[ATTR_LONGITUDE])
|
||||
|
||||
return zone_dist - radius < zone.attributes[ATTR_RADIUS]
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup zone. """
|
||||
entities = set()
|
||||
|
||||
for key in extract_domain_configs(config, DOMAIN):
|
||||
entries = config[key]
|
||||
if not isinstance(entries, list):
|
||||
entries = entries,
|
||||
|
||||
for entry in entries:
|
||||
name = entry.get(CONF_NAME, DEFAULT_NAME)
|
||||
latitude = entry.get(ATTR_LATITUDE)
|
||||
longitude = entry.get(ATTR_LONGITUDE)
|
||||
radius = entry.get(ATTR_RADIUS, DEFAULT_RADIUS)
|
||||
icon = entry.get(ATTR_ICON)
|
||||
|
||||
if None in (latitude, longitude):
|
||||
logging.getLogger(__name__).error(
|
||||
'Each zone needs a latitude and longitude.')
|
||||
continue
|
||||
|
||||
zone = Zone(hass, name, latitude, longitude, radius, icon)
|
||||
zone.entity_id = generate_entity_id(ENTITY_ID_FORMAT, name,
|
||||
entities)
|
||||
zone.update_ha_state()
|
||||
entities.add(zone.entity_id)
|
||||
|
||||
if ENTITY_ID_HOME not in entities:
|
||||
zone = Zone(hass, hass.config.location_name, hass.config.latitude,
|
||||
hass.config.longitude, DEFAULT_RADIUS, ICON_HOME)
|
||||
zone.entity_id = ENTITY_ID_HOME
|
||||
zone.update_ha_state()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Zone(Entity):
|
||||
""" Represents a Zone in Home Assistant. """
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, name, latitude, longitude, radius, icon):
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.radius = radius
|
||||
self.icon = icon
|
||||
|
||||
def should_poll(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" The state property really does nothing for a zone. """
|
||||
return STATE
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
attr = {
|
||||
ATTR_HIDDEN: True,
|
||||
ATTR_LATITUDE: self.latitude,
|
||||
ATTR_LONGITUDE: self.longitude,
|
||||
ATTR_RADIUS: self.radius,
|
||||
}
|
||||
if self.icon:
|
||||
attr[ATTR_ICON] = self.icon
|
||||
return attr
|
||||
+17
-2
@@ -1,6 +1,7 @@
|
||||
# coding: utf-8
|
||||
""" Constants used by Home Assistant components. """
|
||||
|
||||
__version__ = "0.7.2"
|
||||
__version__ = "0.7.4"
|
||||
|
||||
# Can be used to specify a catch all when registering state or event listeners.
|
||||
MATCH_ALL = '*'
|
||||
@@ -40,13 +41,16 @@ STATE_ON = 'on'
|
||||
STATE_OFF = 'off'
|
||||
STATE_HOME = 'home'
|
||||
STATE_NOT_HOME = 'not_home'
|
||||
STATE_UNKNOWN = "unknown"
|
||||
STATE_UNKNOWN = 'unknown'
|
||||
STATE_OPEN = 'open'
|
||||
STATE_CLOSED = 'closed'
|
||||
STATE_PLAYING = 'playing'
|
||||
STATE_PAUSED = 'paused'
|
||||
STATE_IDLE = 'idle'
|
||||
STATE_STANDBY = 'standby'
|
||||
STATE_ALARM_DISARMED = 'disarmed'
|
||||
STATE_ALARM_ARMED_HOME = 'armed_home'
|
||||
STATE_ALARM_ARMED_AWAY = 'armed_away'
|
||||
|
||||
# #### STATE AND EVENT ATTRIBUTES ####
|
||||
# Contains current time for a TIME_CHANGED event
|
||||
@@ -97,6 +101,13 @@ ATTR_LAST_TRIP_TIME = "last_tripped_time"
|
||||
# For all entity's, this hold whether or not it should be hidden
|
||||
ATTR_HIDDEN = "hidden"
|
||||
|
||||
# Location of the entity
|
||||
ATTR_LATITUDE = "latitude"
|
||||
ATTR_LONGITUDE = "longitude"
|
||||
|
||||
# Accuracy of location in meters
|
||||
ATTR_GPS_ACCURACY = 'gps_accuracy'
|
||||
|
||||
# #### SERVICES ####
|
||||
SERVICE_HOMEASSISTANT_STOP = "stop"
|
||||
|
||||
@@ -114,6 +125,10 @@ SERVICE_MEDIA_NEXT_TRACK = "media_next_track"
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK = "media_previous_track"
|
||||
SERVICE_MEDIA_SEEK = "media_seek"
|
||||
|
||||
SERVICE_ALARM_DISARM = "alarm_disarm"
|
||||
SERVICE_ALARM_ARM_HOME = "alarm_arm_home"
|
||||
SERVICE_ALARM_ARM_AWAY = "alarm_arm_away"
|
||||
|
||||
# #### API / REMOTE ####
|
||||
SERVER_PORT = 8123
|
||||
|
||||
|
||||
+43
-8
@@ -26,6 +26,7 @@ from homeassistant.exceptions import (
|
||||
HomeAssistantError, InvalidEntityFormatError)
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.dt as date_util
|
||||
import homeassistant.util.location as location
|
||||
import homeassistant.helpers.temperature as temp_helper
|
||||
from homeassistant.config import get_default_config_dir
|
||||
|
||||
@@ -445,9 +446,8 @@ class StateMachine(object):
|
||||
|
||||
domain_filter = domain_filter.lower()
|
||||
|
||||
return [state.entity_id for key, state
|
||||
in self._states.items()
|
||||
if util.split_entity_id(key)[0] == domain_filter]
|
||||
return [state.entity_id for state in self._states.values()
|
||||
if state.domain == domain_filter]
|
||||
|
||||
def all(self):
|
||||
""" Returns a list of all states. """
|
||||
@@ -524,6 +524,28 @@ class StateMachine(object):
|
||||
from_state, to_state)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class Service(object):
|
||||
""" Represents a service. """
|
||||
|
||||
__slots__ = ['func', 'description', 'fields']
|
||||
|
||||
def __init__(self, func, description, fields):
|
||||
self.func = func
|
||||
self.description = description or ''
|
||||
self.fields = fields or {}
|
||||
|
||||
def as_dict(self):
|
||||
""" Return dictionary representation of this service. """
|
||||
return {
|
||||
'description': self.description,
|
||||
'fields': self.fields,
|
||||
}
|
||||
|
||||
def __call__(self, call):
|
||||
self.func(call)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class ServiceCall(object):
|
||||
""" Represents a call to a service. """
|
||||
@@ -558,20 +580,29 @@ class ServiceRegistry(object):
|
||||
def services(self):
|
||||
""" Dict with per domain a list of available services. """
|
||||
with self._lock:
|
||||
return {domain: list(self._services[domain].keys())
|
||||
return {domain: {key: value.as_dict() for key, value
|
||||
in self._services[domain].items()}
|
||||
for domain in self._services}
|
||||
|
||||
def has_service(self, domain, service):
|
||||
""" Returns True if specified service exists. """
|
||||
return service in self._services.get(domain, [])
|
||||
|
||||
def register(self, domain, service, service_func):
|
||||
""" Register a service. """
|
||||
def register(self, domain, service, service_func, description=None):
|
||||
"""
|
||||
Register a service.
|
||||
|
||||
Description is a dict containing key 'description' to describe
|
||||
the service and a key 'fields' to describe the fields.
|
||||
"""
|
||||
description = description or {}
|
||||
service_obj = Service(service_func, description.get('description'),
|
||||
description.get('fields', {}))
|
||||
with self._lock:
|
||||
if domain in self._services:
|
||||
self._services[domain][service] = service_func
|
||||
self._services[domain][service] = service_obj
|
||||
else:
|
||||
self._services[domain] = {service: service_func}
|
||||
self._services[domain] = {service: service_obj}
|
||||
|
||||
self._bus.fire(
|
||||
EVENT_SERVICE_REGISTERED,
|
||||
@@ -676,6 +707,10 @@ class Config(object):
|
||||
# Directory that holds the configuration
|
||||
self.config_dir = get_default_config_dir()
|
||||
|
||||
def distance(self, lat, lon):
|
||||
""" Calculate distance from Home Assistant in meters. """
|
||||
return location.distance(self.latitude, self.longitude, lat, lon)
|
||||
|
||||
def path(self, *path):
|
||||
""" Returns path to the file within the config dir. """
|
||||
return os.path.join(self.config_dir, *path)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""
|
||||
Helper methods for components within Home Assistant.
|
||||
"""
|
||||
import re
|
||||
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_PLATFORM, DEVICE_DEFAULT_NAME)
|
||||
@@ -73,7 +75,7 @@ def config_per_platform(config, domain, logger):
|
||||
config_key = domain
|
||||
found = 1
|
||||
|
||||
while config_key in config:
|
||||
for config_key in extract_domain_configs(config, domain):
|
||||
platform_config = config[config_key]
|
||||
if not isinstance(platform_config, list):
|
||||
platform_config = [platform_config]
|
||||
@@ -89,3 +91,9 @@ def config_per_platform(config, domain, logger):
|
||||
|
||||
found += 1
|
||||
config_key = "{} {}".format(domain, found)
|
||||
|
||||
|
||||
def extract_domain_configs(config, domain):
|
||||
""" Extract keys from config for given domain name. """
|
||||
pattern = re.compile(r'^{}(| .+)$'.format(domain))
|
||||
return (key for key in config.keys() if pattern.match(key))
|
||||
|
||||
@@ -10,8 +10,8 @@ from collections import defaultdict
|
||||
from homeassistant.exceptions import NoEntitySpecifiedError
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, ATTR_HIDDEN,
|
||||
STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, TEMP_CELCIUS,
|
||||
ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_UNIT_OF_MEASUREMENT,
|
||||
DEVICE_DEFAULT_NAME, STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELCIUS,
|
||||
TEMP_FAHRENHEIT)
|
||||
|
||||
# Dict mapping entity_id to a boolean that overwrites the hidden property
|
||||
@@ -44,17 +44,17 @@ class Entity(object):
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the entity. """
|
||||
return self.get_name()
|
||||
return DEVICE_DEFAULT_NAME
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the entity. """
|
||||
return self.get_state()
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns the state attributes. """
|
||||
return {}
|
||||
return None
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
@@ -64,34 +64,12 @@ class Entity(object):
|
||||
@property
|
||||
def hidden(self):
|
||||
""" Suggestion if the entity should be hidden from UIs. """
|
||||
return self._hidden
|
||||
|
||||
@hidden.setter
|
||||
def hidden(self, val):
|
||||
""" Sets the suggestion for visibility. """
|
||||
self._hidden = bool(val)
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
""" Retrieve latest state. """
|
||||
pass
|
||||
|
||||
# DEPRECATION NOTICE:
|
||||
# Device is moving from getters to properties.
|
||||
# For now the new properties will call the old functions
|
||||
# This will be removed in the future.
|
||||
|
||||
def get_name(self):
|
||||
""" Returns the name of the entity if any. """
|
||||
return DEVICE_DEFAULT_NAME
|
||||
|
||||
def get_state(self):
|
||||
""" Returns state of the entity. """
|
||||
return "Unknown"
|
||||
|
||||
def get_state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
return None
|
||||
|
||||
# DO NOT OVERWRITE
|
||||
# These properties and methods are either managed by Home Assistant or they
|
||||
# are used to perform a very specific function. Overwriting these may
|
||||
|
||||
@@ -129,13 +129,13 @@ class EntityComponent(object):
|
||||
if platform is None:
|
||||
return
|
||||
|
||||
platform_name = '{}.{}'.format(self.domain, platform_type)
|
||||
|
||||
try:
|
||||
platform.setup_platform(
|
||||
self.hass, platform_config, self.add_entities, discovery_info)
|
||||
|
||||
self.hass.config.components.append(platform_name)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
self.logger.exception(
|
||||
'Error while setting up platform %s', platform_type)
|
||||
return
|
||||
|
||||
platform_name = '{}.{}'.format(self.domain, platform_type)
|
||||
self.hass.config.components.append(platform_name)
|
||||
|
||||
@@ -9,7 +9,9 @@ import logging
|
||||
from homeassistant.core import State
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||
STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
|
||||
STATE_PLAYING, STATE_PAUSED, ATTR_ENTITY_ID)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,14 +53,21 @@ def reproduce_state(hass, states, blocking=False):
|
||||
current_state = hass.states.get(state.entity_id)
|
||||
|
||||
if current_state is None:
|
||||
_LOGGER.warning('reproduce_state: Unable to find entity %s',
|
||||
state.entity_id)
|
||||
continue
|
||||
|
||||
if state.state == STATE_ON:
|
||||
if state.domain == 'media_player' and state.state == STATE_PAUSED:
|
||||
service = SERVICE_MEDIA_PAUSE
|
||||
elif state.domain == 'media_player' and state.state == STATE_PLAYING:
|
||||
service = SERVICE_MEDIA_PLAY
|
||||
elif state.state == STATE_ON:
|
||||
service = SERVICE_TURN_ON
|
||||
elif state.state == STATE_OFF:
|
||||
service = SERVICE_TURN_OFF
|
||||
else:
|
||||
_LOGGER.warning("Unable to reproduce state for %s", state)
|
||||
_LOGGER.warning("reproduce_state: Unable to reproduce state %s",
|
||||
state)
|
||||
continue
|
||||
|
||||
service_data = dict(state.attributes)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>org.homeassitant</string>
|
||||
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/usr/local/bin/:/usr/bin:$PATH</string>
|
||||
</dict>
|
||||
|
||||
<key>Program</key>
|
||||
<string>$HASS_PATH$</string>
|
||||
|
||||
<key>AbandonProcessGroup</key>
|
||||
<false/>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
<key>KeepAlive</key>
|
||||
<dict>
|
||||
<key>SuccessfulExit</key>
|
||||
<false/>
|
||||
</dict>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/$USER$/Library/Logs/homeassitant.log</string>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/$USER$/Library/Logs/homeassitant.log</string>
|
||||
|
||||
</dict>
|
||||
</plist>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user