Compare commits

...

1567 Commits

Author SHA1 Message Date
Paulus Schoutsen
74e4b024c0 Merge remote-tracking branch 'origin/dev'
Conflicts:
	Dockerfile
	homeassistant/components/frontend/version.py
	homeassistant/components/frontend/www_static/frontend.html
2015-08-29 23:40:38 -07:00
Paulus Schoutsen
fc6613ffb1 Fix documentation in __main__ 2015-08-29 23:35:19 -07:00
Paulus Schoutsen
3960a465f1 Add conversation and introduction to demo.py 2015-08-29 23:21:26 -07:00
Paulus Schoutsen
a20ab24ba5 Remove unused pip constant 2015-08-29 23:08:33 -07:00
Paulus Schoutsen
f016dec02a Update Dockerfile for new pip requirements 2015-08-29 23:02:36 -07:00
Paulus Schoutsen
fcee2c6d33 Remove unused file 2015-08-29 23:02:14 -07:00
Paulus Schoutsen
a5a1f30798 Make launch more smooth 2015-08-29 23:02:07 -07:00
Paulus Schoutsen
5aa8814a67 Merge pull request #289 from rmkraus/pip_updates
Update dependency installs and prepare for pip deployment
2015-08-29 22:11:20 -07:00
Paulus Schoutsen
7343e33063 Some more frontend bug fixes 2015-08-29 22:02:19 -07:00
Ryan Kraus
bea81ddd92 Minor tweaks
Moved another import to the top of main.
Forced an exit code of 1 when there is an error initializing.
2015-08-29 23:31:33 -04:00
Ryan Kraus
4e01e7ca9b Pip requirements and fixes.
1) Reduced the flags being sent to pip.
2) Required a minimum of pip 7.0.0 for Home Assistant.
2015-08-29 23:17:47 -04:00
Paulus Schoutsen
b750457afa Bugfixes frontend 2015-08-29 20:11:27 -07:00
Ryan Kraus
bfa3900e6a Updated core config directory tests 2015-08-29 22:44:59 -04:00
Ryan Kraus
f5b98c86f0 Mostly PyLint and Flake8 updates.
Rewrote imports of exceptions to be from the exceptions module.
Made nmap scanner check for libnmap dependency without crashing.
Various flake8 and pylint updates.
2015-08-29 22:34:35 -04:00
Ryan Kraus
0b6358e759 Implemented comments from Paulus.
Revised main to use frontend and demo strings rather than importing
their domains.
Removed submodule validation.
Moved local library mounting to the bootstrap module and out of core.
Added requirements_all.txt for all dependencies.
Made core dependencies looser.
Small updates to setup.py.
2015-08-29 22:19:52 -04:00
Ryan Kraus
893ae15042 Changed component REQUIREMENTS to absolute versions. 2015-08-29 21:39:50 -04:00
Paulus Schoutsen
75b3cc046d Update frontend with latest changes 2015-08-29 18:17:21 -07:00
Ryan Kraus
6fdf9b8d7c Many changes to cleanup config directory and lib installations.
Cleaned up default config directory determination.
Made bootstrap creators for HA always set config directory.
Made bootstrap creators set the local library in the Python Path.
Moved all exceptions to their own file to make imports easier.
Moved default configuration directory be in the users’ profile.
Moved pip installs to be done to a lib folder in the config directory.
Reduced requirements.txt to only the barebones reqs.
2015-08-29 21:11:24 -04:00
Ryan Kraus
18e32165a4 Cleaned up main file to remove dependency management. 2015-08-29 20:06:54 -04:00
Ryan Kraus
3839c3d0ef Created setup.py for standard installs. 2015-08-29 18:59:05 -04:00
Paulus Schoutsen
0a2652630f Frontend: Style loading page like login form 2015-08-29 12:01:37 -07:00
Paulus Schoutsen
4b31a22a1c Merge pull request #286 from rmkraus/entity_cleanup
Entity cleanup
2015-08-28 17:25:51 -07:00
Ryan Kraus
c49cdf7ffd Pylint fixes to ISY component. 2015-08-28 20:18:54 -04:00
Ryan Kraus
936e20bdf7 Cleaned up some entities.
1) Modified device tracker entities to allow for attributes to be
overwritten with configuration data.

2) Modified ISY lights to hide brightness when off.
2015-08-28 20:17:07 -04:00
Paulus Schoutsen
6b241f8600 Merge pull request #282 from sfam/dev
Initial support for THOMSON routers
2015-08-28 17:08:03 -07:00
Paulus Schoutsen
4f8b843a1e Merge pull request #284 from MakeMeASandwich/dev
Add support for Adafruit temperature/humidity sensors
2015-08-28 10:42:12 -07:00
MakeMeASandwich
3f4d5eae1c work around build failures also on non-RPi/BB-machines 2015-08-28 18:14:13 +02:00
MakeMeASandwich
76a8bd3969 Add support for Adafruit temperature/humidity sensors 2015-08-28 14:42:41 +02:00
Ryan Kraus
00f3556c34 Merge pull request #283 from rmkraus/dev
Fixed Issue #280
2015-08-27 22:45:38 -04:00
Ryan Kraus
71e60dcfe9 Fixed Issue #280 2015-08-27 22:41:12 -04:00
sfam
4ef4aa2095 fix pylint warnings 2015-08-28 00:09:24 +01:00
sfam
1311e00e90 Initial support for THOMSON routers 2015-08-28 00:03:05 +01:00
sfam
1aef768ff0 Initial support for THOMSON routers 2015-08-28 00:02:26 +01:00
Fabian Affolter
e0db473294 update dnspython 2015-08-27 14:26:06 +02:00
Fabian Affolter
17bf27474a fix dnspython version 2015-08-27 14:25:21 +02:00
Fabian Affolter
387769edff add dnspython 2015-08-27 14:16:29 +02:00
Fabian Affolter
6582067f66 add dnspython to enable xmpp to do srv lookups 2015-08-27 14:15:57 +02:00
Fabian Affolter
f3868ea744 update comments 2015-08-27 14:14:50 +02:00
Paulus Schoutsen
a98ecb6bcc Frontend: material design + introduction card 2015-08-27 01:06:41 -07:00
Paulus Schoutsen
ba7b9c625e Add an introduction component to support first usage 2015-08-27 01:06:41 -07:00
Paulus Schoutsen
e2cfe2a7d2 Merge pull request #278 from sfam/dev
New RPi_GPIO sensor and switch improvements
2015-08-26 08:40:54 -07:00
Paulus Schoutsen
1cb6077e36 Bugfixes for frontend 2015-08-26 08:39:29 -07:00
sfam
47998cff97 Update .coveragerc 2015-08-26 15:45:39 +01:00
sfam
9811869111 fix line too long 2015-08-26 11:27:27 +01:00
sfam
6b3b000822 quick fix 2015-08-26 11:22:06 +01:00
sfam
c8b88219b7 rename config parameter to "invert_logic" 2015-08-26 11:03:06 +01:00
Paulus Schoutsen
884af889a4 Merge pull request #268 from nkgilley/actiontec
device_tracker component: Actiontec MI424WR Verizon FiOS Router
2015-08-25 21:59:38 -07:00
Paulus Schoutsen
c50a47a307 Merge pull request #279 from Zyell/dev
Updated Thermostat unit handling and Nest support
2015-08-25 19:08:52 -07:00
zyell
e15eb90b33 Have thermostat class make all necessary unit conversions 2015-08-25 17:52:40 -07:00
zyell
6647894c36 updated nest to use helper method and updated requirements 2015-08-25 17:52:40 -07:00
zyell
c12b7e70d9 Updated to support Nest API changes and fix set_temp and away_mode and pull in new required python-nest version 2015-08-25 17:52:40 -07:00
sfam
930036272b fix pylint warnings 2015-08-26 00:15:57 +01:00
sfam
c194121da6 fix pylint warnings 2015-08-26 00:11:07 +01:00
sfam
cfc2232c22 fix pylint warnings 2015-08-26 00:02:52 +01:00
sfam
ab5a569922 Add RPi GPIO sensor 2015-08-25 23:24:36 +01:00
sfam
0a9d82fe6f Fix RPi_GPIO switch and add extra parameters 2015-08-25 23:23:51 +01:00
Nolan Gilley
bc5a7564b1 fix formatting 2015-08-25 10:09:47 -04:00
Nolan Gilley
047b4abd82 Fix get_device_name and get_actiontec_data 2015-08-25 09:39:00 -04:00
Paulus Schoutsen
917db18b29 Fix netgear username/password combo 2015-08-24 23:32:07 -07:00
Paulus Schoutsen
c078ee4313 Remove broken Z-Wave support build from Docker 2015-08-24 22:59:05 -07:00
Theodor Lindquist
152fd9cb28 Merge pull request #277 from theolind/dev
Added test for Automation component service id list
2015-08-25 06:55:01 +02:00
theolind
089cd0ff8a Added test for Automation component service id list 2015-08-25 06:50:20 +02:00
Paulus Schoutsen
8cda3f8291 Fix frontend compilation 2015-08-24 21:45:15 -07:00
Theodor Lindquist
83794765a4 Merge pull request #276 from theolind/dev
Added support for YAML lists in automation component confg
2015-08-24 21:35:32 +02:00
theolind
a7889ef628 Automation component now supports YAML lists for config key service_entity_id 2015-08-24 21:19:47 +02:00
Paulus Schoutsen
05df84d04f Frontend bugfix url-sync 2015-08-23 22:45:00 -07:00
Nolan Gilley
93bd238be5 add return None for get_actiontec_data 2015-08-24 00:27:26 -04:00
Paulus Schoutsen
a419509893 More bugfixes frontend 2015-08-23 19:47:51 -07:00
Paulus Schoutsen
d45a7e2ba4 Update demo data to show more features 2015-08-23 19:46:18 -07:00
Paulus Schoutsen
6338f387d2 Bugfixes for frontend 2015-08-23 17:20:23 -07:00
Paulus Schoutsen
35489998df Allow for Netgear router discovery 2015-08-23 17:20:09 -07:00
Paulus Schoutsen
e917479fba Update netdisco to latest version 2015-08-23 17:18:52 -07:00
Paulus Schoutsen
517d4b35ed Update Cast dependency to latest version 2015-08-23 17:18:21 -07:00
Paulus Schoutsen
d69bb8db6e Frontend: fix sensor label and menu button 2015-08-23 10:20:54 -07:00
Paulus Schoutsen
b79d0f5404 Fix discovery and wemo 2015-08-23 10:06:54 -07:00
Paulus Schoutsen
387bdd4a30 Merge pull request #273 from balloob/UI2015
Initial UI revamp
2015-08-23 01:35:48 -07:00
Paulus Schoutsen
33b007b7b4 Initial UI revamp 2015-08-23 01:33:19 -07:00
Paulus Schoutsen
3b982e25a5 Remove broken Z-Wave support build from Docker 2015-08-22 23:32:09 -07:00
Paulus Schoutsen
584e67a6d4 Merge pull request #270 from sfam/dev
Add MQTT switch and sensor
2015-08-22 12:34:36 -07:00
sfam
d0c674b756 update 2015-08-22 16:10:29 +01:00
sfam
fd7808e6f4 update 2015-08-22 16:04:55 +01:00
sfam
a9a650edb6 update style issues 2015-08-22 15:10:49 +01:00
sfam
01ed3b18cc update docstring 2015-08-22 14:57:57 +01:00
Paulus Schoutsen
e09e78347b Upgrade remote test 2015-08-21 21:51:54 -07:00
sfam
8a63325abe update 2015-08-20 23:09:11 +01:00
sfam
44263752ca update header 2015-08-20 23:05:51 +01:00
Nolan Gilley
63e441c73f fix scan_devices 2015-08-20 10:35:01 -04:00
Paulus Schoutsen
051e2db9b7 Merge pull request #272 from nkgilley/scheduler_update
Scheduler update
2015-08-19 13:23:10 -07:00
Paulus Schoutsen
b418907598 Merge pull request #271 from rmkraus/notifier_update
Updated notifier component.
2015-08-19 13:20:47 -07:00
Nolan Gilley
ca515615b9 add support for recording decive name as ip address 2015-08-19 09:52:47 -04:00
Nolan Gilley
7127ddf0a0 fix for broken scheduler component. 2015-08-19 09:16:45 -04:00
Ryan Kraus
a23ab44a6d Removed unnecessary import in notify component. 2015-08-18 22:49:27 -04:00
Ryan Kraus
b8b5ac0653 Removed unnecessary line from notify component. 2015-08-18 22:42:01 -04:00
Ryan Kraus
159411df8b Updated notifier component.
This update to the notifier component allows multiple notifiers to be
configured. In order to support this, an optional property “name” has
been added to all the notifier’s configurations. The notifier can now
be called with the service “notify.NAME”. If the name is not provided,
the service will be mapped to “notify.notify”. Because of this, this
update should be fully backwards compatible.
2015-08-18 22:28:40 -04:00
sfam
c5db42677a Add MQTT Sensor 2015-08-19 01:25:05 +01:00
sfam
2bb4a53bd3 Add MQTT Switch 2015-08-19 01:24:40 +01:00
Paulus Schoutsen
74308b2677 Upgrade nmap dependency 2015-08-18 14:37:53 -07:00
Nolan Gilley
6a830e3b90 fix for flake8 2015-08-18 17:14:26 -04:00
Nolan Gilley
b84d5760eb add to .coveragerc and try again to fix warnings. 2015-08-18 17:03:13 -04:00
Nolan Gilley
c471e39fa0 trying to fix more warnings... 2015-08-18 16:50:40 -04:00
Nolan Gilley
d2f01174e7 fixed warnings 2015-08-18 16:41:03 -04:00
Nolan Gilley
4d5f3da08b Initial commit for device_tracker component for Actiontec MI424WR Verizon FiOS Wireless Router 2015-08-18 16:12:01 -04:00
Paulus Schoutsen
199a80dcfb Merge pull request #267 from balloob/master
Syncing dev with master
2015-08-18 13:04:26 -07:00
Paulus Schoutsen
bf9d067a7d Configure Cast platform host via config 2015-08-17 22:38:10 -07:00
Paulus Schoutsen
ebfec2d1d3 Update update script 2015-08-17 20:11:37 -07:00
Paulus Schoutsen
dfc7f8b0c6 Merge pull request #263 from balloob/dev
Update master with latest changes
2015-08-17 19:55:50 -07:00
Paulus Schoutsen
4ab75d58f5 Update coveragerc 2015-08-17 19:49:08 -07:00
Paulus Schoutsen
b93516197c Merge pull request #261 from persandstrom/verisure
Verisure: Hygrometers etc.
2015-08-17 07:48:02 -07:00
Per Sandstrom
4707b122cc hygrometers and disabling of components 2015-08-17 13:05:49 +02:00
Paulus Schoutsen
086961d109 Add temperature util, helpers 2015-08-16 22:06:01 -07:00
Paulus Schoutsen
b61b3c611d Extract temperature util 2015-08-16 21:36:33 -07:00
Paulus Schoutsen
dda399fc76 Merge pull request #260 from balloob/fix-run-as-module
Extract core from __init__ to core package
2015-08-16 21:23:46 -07:00
Paulus Schoutsen
e984eedffd Fix mysensors import 2015-08-16 21:06:50 -07:00
Paulus Schoutsen
6e41eee6cd Merge pull request #257 from persandstrom/verisure
Support for Verisure
2015-08-16 21:06:24 -07:00
Paulus Schoutsen
9522eac837 Fix mysensors import 2015-08-16 20:53:34 -07:00
Paulus Schoutsen
8bb189e014 Finish core extraction 2015-08-16 20:53:17 -07:00
Paulus Schoutsen
1b89a502c4 Extract core into own submodule 2015-08-16 20:44:46 -07:00
Per Sandstrom
e37869616b no more duplicate sensors 2015-08-16 08:03:19 +02:00
Per Sandstrom
a0f2f3814b fixed flak8 2015-08-16 06:51:09 +02:00
Per Sandstrom
ad327b64ed code reveiw 2015-08-15 13:36:30 +02:00
Per Sandstrom
f20be1e7f8 fixed pylint warning 2015-08-12 13:32:15 +02:00
Per Sandstrom
1b59859681 fixed pylint warning 2015-08-12 13:28:22 +02:00
Per Sandstrom
c7ca6e4784 Added a switch 2015-08-12 13:00:47 +02:00
Fabian Affolter
d0fc91d84a update header 2015-08-11 19:12:32 +02:00
Fabian Affolter
19b62c1088 update header and docstrings 2015-08-11 19:12:32 +02:00
Fabian Affolter
bb848e7fcd update header 2015-08-11 19:12:32 +02:00
Fabian Affolter
06a40ad30c update header 2015-08-11 19:12:32 +02:00
Fabian Affolter
2289d3e826 upadte header 2015-08-11 19:12:32 +02:00
Fabian Affolter
c900836410 update header and docstring 2015-08-11 19:12:32 +02:00
Fabian Affolter
bd373a4d25 update header 2015-08-11 19:12:32 +02:00
Paulus Schoutsen
60abaa585c Add test for bootstrap 2015-08-11 08:20:13 -07:00
Paulus Schoutsen
27ca611689 Fix MQTT coverage directives 2015-08-11 07:49:29 -07:00
Fabian Affolter
2a9616e88c add new entries 2015-08-11 14:38:56 +02:00
Per Sandstrom
92fc7eab36 added component and sensor 2015-08-11 09:28:07 +02:00
Paulus Schoutsen
3fad4d8cda Increase test coverage MQTT 2015-08-10 23:34:58 -07:00
Paulus Schoutsen
65a4b3c9f8 Add MQTT component tests 2015-08-10 23:11:57 -07:00
Paulus Schoutsen
291cc62381 Add automation mqtt tests 2015-08-10 23:11:46 -07:00
Paulus Schoutsen
eecc51c92d Add tests for automation component 2015-08-10 22:26:12 -07:00
Paulus Schoutsen
f79567f7db MQTT: add support for + wildcard in subscribe 2015-08-10 21:49:32 -07:00
Paulus Schoutsen
6e98e55f6a MQTT: add support for + wildcard in subscribe 2015-08-10 21:49:19 -07:00
Paulus Schoutsen
969fe1f3b9 Fix MQTT wildcard topic subscriptions 2015-08-10 18:13:02 -07:00
Paulus Schoutsen
3bbeeda3ac Fix MQTT wildcard topic subscriptions 2015-08-10 18:12:44 -07:00
Paulus Schoutsen
9c63ba1cc3 Merge pull request #252 from chrisvis/tplink-ArcherC9-support
Add support for TP-Link ArcherC9 to to device tracker component
2015-08-10 12:24:38 -07:00
Chris Mulder
58fcf79340 Put new TP-Link device tracker class in same file as original and use the new one and have tailback to original one. 2015-08-10 20:03:43 +02:00
Paulus Schoutsen
ee73bd7dea Merge pull request #251 from balloob/dev
Update master with latest changes
2015-08-09 23:50:52 -07:00
Paulus Schoutsen
8f369d0c27 Merge pull request #254 from auchter/led-color
limitlessled: add color support
2015-08-09 17:56:09 -07:00
Michael Auchter
55c778ca0a add missing docstring 2015-08-10 00:30:47 +00:00
Michael Auchter
f49e5514d6 limitlessled: add color support
Add support for setting the color of a LimitlessLED light.
Currently this implementation is limited to the subset of 16 colors
exposed by the ledcontroller package that is used to interact with the
light itself. Technically the lights themselves support 255 colors.
2015-08-10 00:21:38 +00:00
Paulus Schoutsen
044d43b7c2 Adding MQTT automation rule 2015-08-09 17:12:25 -07:00
Paulus Schoutsen
ae06267072 Merge pull request #253 from auchter/led-brightness
limitlessled: scale brightness to meet ledcontroller expectations
2015-08-09 12:40:57 -07:00
Paulus Schoutsen
d412b51754 MQTT: Add some error reporting 2015-08-09 12:22:05 -07:00
Michael Auchter
4bbe716710 limitlessled: scale brightness to meet ledcontroller expectations
LedController's set_brightness() method expects either an int between 0 and
100, or a float between 0.0 and 1.0, but the brightness here is an int between
0-255. Scale the brightness appropriately.
2015-08-09 14:05:20 -05:00
Paulus Schoutsen
277cdbbe00 MQTT: Add support for authentication 2015-08-09 11:40:23 -07:00
Paulus Schoutsen
c8b54d7468 Update MQTT component and add example 2015-08-09 11:29:50 -07:00
Chris Mulder
2c9c79ea61 Add support for TP-Link ArcherC9 to to device tracker component 2015-08-09 18:23:42 +02:00
Paulus Schoutsen
a2c6dbf479 Clean up MQTT component 2015-08-08 23:49:38 -07:00
Paulus Schoutsen
b37471af68 Update modbus dependency 2015-08-08 21:42:17 -07:00
Paulus Schoutsen
60ade75031 Add missing dependencies 2015-08-08 21:22:34 -07:00
Paulus Schoutsen
7461bf4099 Update coveragerc 2015-08-08 13:52:27 -07:00
Paulus Schoutsen
fc6d7db81b Readd setting up groups with comma seperated list 2015-08-08 13:44:07 -07:00
Paulus Schoutsen
0901ed4659 Merge pull request #242 from persandstrom/squeezebox
Squeezebox
2015-08-08 12:30:45 -07:00
Paulus Schoutsen
a0685c69f5 Merge pull request #250 from fabaff/mqtt-new
MQTT component
2015-08-08 10:47:38 -07:00
Fabian Affolter
87bf3c4e0d fix a couple of typos and use port(s) everywhere 2015-08-08 19:20:53 +02:00
Fabian Affolter
1f3bde3e08 update header 2015-08-08 19:16:15 +02:00
Fabian Affolter
6268a9f5b2 update header 2015-08-08 19:09:37 +02:00
Fabian Affolter
26dbb5ca3f make some entries optional, update payload handling, and use qos 2015-08-08 18:52:59 +02:00
Paulus Schoutsen
ac14698ab9 Updated version of frontend 2015-08-07 22:02:30 -07:00
Fabian Affolter
eac5b19309 add paho-mqtt 2015-08-07 19:22:27 +02:00
Fabian Affolter
a9d2adea45 add mqtt 2015-08-07 19:22:17 +02:00
Fabian Affolter
641d3f5e01 add mqtt component 2015-08-07 19:20:27 +02:00
Fabian Affolter
f7dc438d10 update header 2015-08-06 22:32:53 +02:00
Fabian Affolter
c987251585 update header 2015-08-06 22:32:41 +02:00
Fabian Affolter
dff626fb2d add requirement 2015-08-06 22:28:46 +02:00
Fabian Affolter
fce16c6cdd update header 2015-08-06 22:27:45 +02:00
Fabian Affolter
f84b3a509d update header 2015-08-06 22:24:23 +02:00
Fabian Affolter
22e30dc85a update header 2015-08-06 19:15:37 +02:00
Fabian Affolter
a1df7b9b26 add periods 2015-08-06 19:00:47 +02:00
Fabian Affolter
8029d3e592 remove some blank lines 2015-08-06 19:00:18 +02:00
Fabian Affolter
2eb3a5af3b update header 2015-08-06 18:53:44 +02:00
Fabian Affolter
22fa9831d8 update header 2015-08-06 18:45:00 +02:00
Fabian Affolter
b39ae8d23a update header 2015-08-06 18:44:48 +02:00
Fabian Affolter
6b30fc714c update header 2015-08-06 18:34:20 +02:00
Fabian Affolter
9d5c7ceecd update header 2015-08-06 18:32:29 +02:00
Fabian Affolter
e8dd5779f6 update header 2015-08-06 18:31:35 +02:00
Fabian Affolter
aa8ec4724b update header 2015-08-06 18:23:26 +02:00
Fabian Affolter
b946b3b2bc update header 2015-08-06 18:23:10 +02:00
Fabian Affolter
5fa34b10b3 update file header 2015-08-06 17:54:05 +02:00
Fabian Affolter
caecca7e1f update file header 2015-08-06 17:53:49 +02:00
Fabian Affolter
83cfb1c861 add slack 2015-08-06 16:57:36 +02:00
Paulus Schoutsen
46fd23c452 Merge pull request #244 from balloob/cleanup
Core Cleanup
2015-08-06 05:23:23 -07:00
Paulus Schoutsen
7c61e00948 Merge pull request #239 from gbarba/rpi_gpio_component
Add rpi_gpio platform to switch
2015-08-06 05:20:53 -07:00
Paulus Schoutsen
b0065f7a95 Merge pull request #249 from persandstrom/asuswrt
ASUSWRT - Exception when client not in leases
2015-08-06 04:04:39 -07:00
Guillem Barba
91961e629f Resolve pylint errors and warnings 2015-08-06 09:57:16 +02:00
Guillem Barba
e6b4dba330 Remove usless comments 2015-08-06 09:57:16 +02:00
Guillem Barba
d6c7bf5ac8 Remove usless DEMO constant. Add version information in RPi.GPIO requirement 2015-08-06 09:57:16 +02:00
Guillem Barba
e97e73e66e Add rpi_gpio platform to switch 2015-08-06 09:57:16 +02:00
Per Sandstrom
b681cf2eaa removed unnecessary log row 2015-08-06 09:27:23 +02:00
Per Sandstrom
65cc0954c8 Exception when client not in leases
Client can be dropped from leases list from where the status is
retreived before it is dropped from the ip neigh list.
The client needs a default status.
2015-08-06 09:19:23 +02:00
Per Sandstrom
9ead39e703 added turn on/off support 2015-08-06 09:05:27 +02:00
Paulus Schoutsen
ac19ac8b83 Update frontend dependencies 2015-08-05 21:30:58 -07:00
Paulus Schoutsen
c4f71df1b2 Merge pull request #246 from balloob/fix-forecastio
Fix sensor.forecastio with Fahrenheit values
2015-08-05 17:00:12 -07:00
Per Sandstrom
393e88e732 add to .coveragerc 2015-08-05 22:25:03 +02:00
Paulus Schoutsen
450b510d08 Fix sensor.forecastio to treat Fahrenheit wrong
Fixes #245
2015-08-05 11:55:59 -07:00
Per Sandstrom
03f93063f8 fixed flake8 issues 2015-08-05 20:09:20 +02:00
Per Sandstrom
eb83621fce fixing pylint issues 2015-08-05 20:02:39 +02:00
Paulus Schoutsen
b2cfce7243 Merge branch 'dev' into cleanup
Conflicts:
	.coveragerc
2015-08-05 06:51:39 -07:00
Paulus Schoutsen
2eeb80f173 Merge branch 'pr/229' into dev
Conflicts:
	requirements.txt
2015-08-05 06:48:14 -07:00
Paulus Schoutsen
086e786b28 Merge branch 'pr/238' into dev
Conflicts:
	requirements.txt
2015-08-05 06:45:56 -07:00
Paulus Schoutsen
23f0195619 Merge pull request #241 from vitorespindola/master
Modbus coil support
2015-08-05 15:40:49 +02:00
Paulus Schoutsen
a163f2da2d Merge pull request #243 from persandstrom/asuswrt
ASUSWRT based routers
2015-08-05 15:39:48 +02:00
Per Sandstrom
db2cbf33c3 Added support for multiple players 2015-08-05 13:49:45 +02:00
Per Sandstrom
52ec4ac1d8 flake8 and pylint 2015-08-05 10:22:03 +02:00
Paulus Schoutsen
4f9fa7f8ad Update coveragerc 2015-08-04 16:33:35 -04:00
Paulus Schoutsen
aa20b94927 Remove support for old home-assistant.conf file 2015-08-04 16:21:25 -04:00
Paulus Schoutsen
d2b5f429fe Remove deprecated code 2015-08-04 16:21:09 -04:00
Per Sandstrom
30e24296c4 Fixed flake8 2015-08-04 20:30:01 +02:00
Per Sandstrom
e6c09f7413 Fixed bug with password protected LMS 2015-08-04 20:08:48 +02:00
Per Sandstrom
4284a3f5dc Fixed pylint conventions 2015-08-04 19:35:53 +02:00
Paulus Schoutsen
2075de3d81 Extended test_init tests to cover all 2015-08-04 18:16:10 +02:00
Paulus Schoutsen
df3ee6005a Nicer test imports
unittest changes import path so old style worked but is confusing
2015-08-04 18:15:22 +02:00
Paulus Schoutsen
14023a15e6 Minor code cleanup 2015-08-04 18:13:55 +02:00
Paulus Schoutsen
e47ac96587 Remove more deprecated method calls 2015-08-04 18:13:35 +02:00
Per Sandstrom
bed30a5307 added support for logitech squeezebox 2015-08-04 17:22:56 +02:00
Paulus Schoutsen
76f63ee262 Clean up test code 2015-08-03 17:57:12 +02:00
Paulus Schoutsen
4096a67251 Built-in component cleanup 2015-08-03 17:42:28 +02:00
Paulus Schoutsen
382c1de981 Built-in components no longer use deprecated methods 2015-08-03 17:08:13 +02:00
Paulus Schoutsen
7870e9a5e2 Minor cleanup core 2015-08-03 17:05:33 +02:00
James Cole
99bc7a997a Merge pull request #236 from jamespcole/slack-dev
Slack notifier component for sending messages to a slack channel.
2015-08-03 14:36:34 +10:00
Rohit Kabadi
65d32c7425 Added blank line to temper.py 2015-08-02 18:58:30 -07:00
Rohit Kabadi
6c6ae9cb1a Added REQUIREMENTS list to temper.py 2015-08-02 18:55:30 -07:00
Rohit Kabadi
e6aabb9706 Fixed flake8 violations 2015-08-02 18:51:13 -07:00
Rohit Kabadi
3d57c80656 Merge remote-tracking branch 'upstream/dev' into temper 2015-08-02 18:18:18 -07:00
Rohit Kabadi
c248d5455e Added REQUIREMENTS lilst to edimax.py 2015-08-02 18:01:31 -07:00
Rohit Kabadi
0e153183d4 Added name for temper, added to coveragerc, 2015-08-01 12:46:28 -07:00
Rohit Kabadi
3c08a5ee6e Added support for temper temperature sensors 2015-08-01 12:20:29 -07:00
Vitor Espíndola
f97b7c9e61 Merge branch 'master' of https://github.com/vitorespindola/home-assistant
Conflicts:
	homeassistant/components/sensor/modbus.py
	homeassistant/components/switch/modbus.py
2015-08-01 10:01:37 -03:00
Vitor Espíndola
1b3a45aba9 Modbus coil support 2015-08-01 09:54:32 -03:00
jamespcole
6873504cc0 Fixed linting errors 2015-08-01 06:45:41 +10:00
Rohit Kabadi
ffde7e183e Fixed flake8 violations 2015-07-30 21:05:00 -07:00
Per Sandstrom
ed0164843a Added support for ASUSWRT based routers 2015-07-30 11:30:31 +02:00
Rohit Kabadi
f351ab9544 Updated branch to avoid conflicts in requirements.txt 2015-07-30 00:37:11 -07:00
Rohit Kabadi
a99484ebc8 Merge remote-tracking branch 'upstream/dev' into edimax_smart_plug
Conflicts:
	requirements.txt
2015-07-30 00:31:26 -07:00
Rohit Kabadi
6a239bf18a Used validate_config to ensure 'host' parameter in edimax config. Added name option to edimax config 2015-07-30 00:10:16 -07:00
Vitor Espíndola
f82b63483a Modbus coil support 2015-07-29 14:04:32 -03:00
Rohit Kabadi
f6811e858a - Removed https://github.com/rkabadi/pyedimax as submodule
- Added https://github.com/rkabadi/pyedimax to requirements
- Modified edimax.py to import pyedimax from python3 default packages
2015-07-29 00:24:42 -07:00
Fabian Affolter
c1b428489f fix requirement 2015-07-27 18:58:32 +02:00
Fabian Affolter
5eb40be474 Merge branch 'dev' of github.com:fabaff/home-assistant into dev 2015-07-27 18:43:13 +02:00
Fabian Affolter
4845c1290c remove unused stuff and update the names (same as in owm sensor) 2015-07-27 18:41:03 +02:00
Paulus Schoutsen
e0468f8b8e Extract helpers.event from core + misc cleanup 2015-07-26 10:45:49 +02:00
Paulus Schoutsen
0c56fde5a9 Reorg tests folder 2015-07-26 10:17:01 +02:00
Paulus Schoutsen
fed36d2cd0 Better error reporting remote classes 2015-07-26 00:14:55 -07:00
Rohit Kabadi
613c0122c0 - Reverted submodule updates. This is the 2nd attempt since the first one did not work 2015-07-26 00:08:57 -07:00
Rohit Kabadi
bb0ace3a61 - Reverted submodule updates 2015-07-25 23:59:48 -07:00
Paulus Schoutsen
c659be7e17 Sun component will work now without internet 2015-07-25 23:45:49 -07:00
Rohit Kabadi
6a7e28cc85 - Added support for getting power on SP2101W devices (returns None on SP1101W) 2015-07-25 18:46:47 -07:00
Paulus Schoutsen
c1b6d03d1b Merge remote-tracking branch 'origin/remove-external' into dev 2015-07-24 04:11:31 -07:00
Paulus Schoutsen
37ec18b363 Merge pull request #226 from balloob/rfxcom
Support for rfxtrx sensors
2015-07-24 04:10:23 -07:00
Daniel Hoyer Iversen
3658c57912 updated rfxcom sensor 2015-07-24 13:06:15 +02:00
Daniel Hoyer Iversen
1489af0eca updated rfxcom sensor 2015-07-24 12:35:03 +02:00
Paulus Schoutsen
445aaeb700 New compiled version of frontend 2015-07-24 03:28:21 -07:00
Paulus Schoutsen
d33af6e83e Merge pull request #227 from balloob/thermostat
Custom min/max temperature for thermostat
2015-07-24 03:24:49 -07:00
Paulus Schoutsen
acd51268fc Merge pull request #228 from balloob/tellstick_bug
Make  only_named: 0  work
2015-07-24 03:23:55 -07:00
Daniel Hoyer Iversen
22c72060cf Make only_named: 0 work 2015-07-23 22:24:48 +02:00
Daniel Hoyer Iversen
be937a795a Min max temp for thermostat 2015-07-23 22:15:17 +02:00
Daniel Hoyer Iversen
b54c58235f Documentation of rfxtrx sensor 2015-07-23 19:50:26 +02:00
Daniel Hoyer Iversen
8f99ebf27e Documentation of rfxtrx sensor 2015-07-23 19:47:45 +02:00
Daniel Hoyer Iversen
f44acc9b0e requirements file 2015-07-23 19:42:20 +02:00
Daniel Hoyer Iversen
44ce756cba Support for rfxtrx sensors 2015-07-23 19:36:05 +02:00
Daniel Hoyer Iversen
cbb390a918 Custom min/max temperature for thermostat 2015-07-23 18:13:46 +02:00
Rohit Kabadi
fac194f66c - Added for smartplug
- Added error check for host param in config.yaml
- Fixed SmartPlugSwitch is_on method
- Edimax smartplug works now!
2015-07-20 23:27:25 -07:00
Rohit Kabadi
6631ebfdfa - Added git submodule @ https://github.com/rkabadi/pyedimax
- Added edimax.py module to interface with Edimax SP-1101W and SP-2101W
2015-07-20 20:16:54 -07:00
Paulus Schoutsen
dc2ed19105 Merge pull request #223 from balloob/dev
Update master with latest changes
2015-07-20 00:56:54 -07:00
Paulus Schoutsen
40b2acb472 Port wink from external to requirements.txt 2015-07-20 00:41:57 -07:00
Paulus Schoutsen
2f622053a6 Port PyWemo from external to requirements.txt 2015-07-20 00:08:00 -07:00
Paulus Schoutsen
3efb1e4ac9 Port netdisco from external to requirements.txt 2015-07-20 00:07:01 -07:00
Paulus Schoutsen
43cc3624ee Port PyNetgear from external to requirements.txt 2015-07-19 23:44:32 -07:00
Paulus Schoutsen
4edf53899d Port PyMySensors from external to requirements.txt 2015-07-19 23:25:13 -07:00
Paulus Schoutsen
e277decd4c Add a check to ensure pip is installed 2015-07-19 19:00:48 -07:00
Paulus Schoutsen
a3906242e9 Make life of Windows users a bit better 2015-07-19 18:32:35 -07:00
Paulus Schoutsen
7dba1b5303 Remove comment from build_frontend 2015-07-17 23:42:10 -07:00
Paulus Schoutsen
f50a6fc24c Update to latest Home Assistant Polymer 2015-07-17 23:38:25 -07:00
Paulus Schoutsen
f4562fa352 Merge pull request #222 from balloob/change-dependency-sun
Sun component: ephem->astral
2015-07-16 21:35:06 -07:00
Paulus Schoutsen
1fda362ca3 Take elevation into consideration 2015-07-16 21:19:23 -07:00
Paulus Schoutsen
35f0270688 Sun component: ephem->astral 2015-07-16 19:57:07 -07:00
Paulus Schoutsen
4d12c69d68 Increase robustness dependency installation 2015-07-15 18:38:48 -07:00
Paulus Schoutsen
c532a28a98 Increase robustness dependency installation 2015-07-15 18:37:24 -07:00
Paulus Schoutsen
34b6627f9a Merge pull request #216 from balloob/polymer-es6
Extract frontend code into own repository
2015-07-12 23:06:11 -07:00
Paulus Schoutsen
d3a6190044 Update frontend to latest version 2015-07-12 22:58:35 -07:00
Paulus Schoutsen
0bf45653e3 Upgrade to new version frontend 2015-07-12 21:08:39 -07:00
Paulus Schoutsen
87961c1c53 Remove old dev scripts 2015-07-12 21:08:25 -07:00
Paulus Schoutsen
c861622748 Extract frontend code into own repository 2015-07-12 20:43:07 -07:00
Paulus Schoutsen
eb630d61db Merge pull request #215 from rmkraus/dev
Updating Debian daemon script for Python Virtual Environments
2015-07-12 16:02:46 -07:00
Ryan Kraus
7fcdb9b975 Updating Debian daemon script for Python Virtual Environments
Fix to init.d script that forces the correct virtual environment to be
loaded.
2015-07-12 18:44:38 -04:00
Paulus Schoutsen
bfa8131f4b Merge branch 'dev' 2015-07-11 12:28:28 -07:00
Paulus Schoutsen
27850ef5df Frontend: hide frontend specific attributes from default more info content 2015-07-11 12:28:11 -07:00
Paulus Schoutsen
c4a4aceeeb Frontend: Minor UI tweaks for Camera 2015-07-11 11:55:25 -07:00
Paulus Schoutsen
33b7585e0c Merge pull request #202 from balloob/dev
Update master with latest changes
2015-07-11 02:00:18 -07:00
Paulus Schoutsen
ef370034b6 New frontend build 2015-07-11 01:56:58 -07:00
Paulus Schoutsen
eb4bb6925f Stub http component in tests 2015-07-11 00:02:52 -07:00
Paulus Schoutsen
dea0fcc845 Improve virtual env detection 2015-07-11 00:02:42 -07:00
Paulus Schoutsen
2cbfc60679 Add camera to demo 2015-07-10 23:24:05 -07:00
Paulus Schoutsen
aec25c88b4 Clean up camera component 2015-07-10 23:17:12 -07:00
Paulus Schoutsen
703266312e Exclude efergy sensor for coverage 2015-07-10 22:34:02 -07:00
Paulus Schoutsen
c90c32eee8 Simplify README 2015-07-10 22:33:51 -07:00
Paulus Schoutsen
c3402dca4e Upgrade travis.yml to run on new infra 2015-07-10 22:00:13 -07:00
Paulus Schoutsen
590b6ba6e7 Merge pull request #203 from balloob/auto-dependency
Automatic dependency management
2015-07-10 21:53:10 -07:00
Paulus Schoutsen
d2417768ce Merge pull request #170 from jamespcole/simple-camera-dev
Very simple IP Camera support
2015-07-10 21:52:31 -07:00
Paulus Schoutsen
b6fd282143 Merge remote-tracking branch 'origin/dev' into auto-dependency
Conflicts:
	homeassistant/components/media_player/cast.py
2015-07-10 21:48:01 -07:00
Paulus Schoutsen
afeb2cfc09 Update package requirement for Chromecast 2015-07-10 21:35:48 -07:00
Paulus Schoutsen
b6c710585b Merge pull request #214 from rmkraus/dev
Created init.d script
2015-07-10 21:35:08 -07:00
Paulus Schoutsen
9af3ff6d9d Update README 2015-07-10 21:32:02 -07:00
Ryan Kraus
b3479a4ab6 Created init.d script
Created a Debian init.d compatible service script.
2015-07-11 00:28:37 -04:00
Paulus Schoutsen
2b23eec0f7 Fix pip 1.5.4 compatibility 2015-07-10 21:24:28 -07:00
Paulus Schoutsen
8363757f1c Merge pull request #212 from rmkraus/dev
IGNORE_CEC Parameter for Cast device
2015-07-10 21:16:36 -07:00
Paulus Schoutsen
a600d67dfc Merge pull request #211 from miniconfig/efergy-dev
Added a new sensor to read data from the efergy energy monitor
2015-07-10 20:57:36 -07:00
jamespcole
bf2b06880e Fixed some linting errors 2015-07-10 20:10:23 +10:00
jamespcole
9a63f34129 Rebuilt with updated bower components 2015-07-10 19:57:30 +10:00
jamespcole
0ca836d7ed Another frontend rebuild 2015-07-10 19:42:22 +10:00
jamespcole
a85b47805f rebuilt frontend with camera component changes 2015-07-10 18:44:00 +10:00
jamespcole
3440c54ab7 Merged in upstream changes 2015-07-10 18:14:03 +10:00
jamespcole
4b2d10a741 Added constants for camera streaming 2015-07-10 18:04:17 +10:00
jamespcole
c231a349c7 Updates from pull request feedback 2015-07-10 18:03:46 +10:00
Ryan Kraus
f3ff8ca9ca Bumped PyChromecast version in requirements
Bumped PyChromecast version to a hypothetical 0.6.9 since the newest is
already 0.6.8.
2015-07-10 00:54:29 -04:00
Ryan Kraus
1bfde8a1e5 Rolling home-assistant-js back to newest
Accidentally rolled home-assistant-js back. Brought it back to the
current version.
2015-07-10 00:34:52 -04:00
Ryan Kraus
67135a7150 Implimented Ignore CEC for Chromecasts
1) Added the ability to ignore CEC data from Chromecasts using pending
updates to PyChromecast library.
2) Modified cast device to not allow the same device to be imported
twice. This can happen when cast is setup as a media_player in the
config when the discovery component is active.
2015-07-10 00:29:07 -04:00
miniconfig
18bcf3ea00 Added a new sensor to read data from the efergy energy monitor 2015-07-09 12:05:19 -04:00
Paulus Schoutsen
336d0a3972 Merge pull request #209 from michaelarnauts/hue_colorloop
Implement the colorloop effect for hue lights
2015-07-08 19:37:08 -07:00
Paulus Schoutsen
045e0c70cb Merge pull request #210 from balloob/tellstick-robustness
Added functionallity so that the tellstick switch can send its signals repeatedly
2015-07-08 19:35:16 -07:00
Gustav Ahlberg
370355b94b Added functionallity so that the tellstick switch can send it's signals repeatedly
Because the tellstick sends its actions via radio and from most receivers it's impossible to know if the signal was received or not.
2015-07-08 21:39:50 +02:00
Michaël Arnauts
7530109ce8 Implement the colorloop effect for hue lights 2015-07-08 20:26:37 +02:00
Paulus Schoutsen
33e983a5c3 Update ISY994 dependency 2015-07-08 00:01:10 -07:00
Paulus Schoutsen
d7e46b5427 Merge pull request #205 from michaelarnauts/dev
Use xy_color instead of color in the configuration example
2015-07-07 23:49:44 -07:00
Paulus Schoutsen
b86e9a4fc1 Merge pull request #207 from rmkraus/isy_dev
Updating the PyISY dependency
2015-07-07 23:49:21 -07:00
Paulus Schoutsen
785e5e0fe7 Merge pull request #208 from rmkraus/isy_dev_master
Light update to PyISY version 1.0.5
2015-07-07 21:30:04 -07:00
Ryan Kraus
d1e4387997 Light update to PyISY version 1.0.5
This is a lighter update to version 1.0.5 to fix Issue #201 more
immediately.
2015-07-07 23:19:34 -04:00
Ryan Kraus
237778a8bc Update to PyISY 1.0.5
Updated Home Assistant to use PyISY version 1.0.5 to fix error when no
climate module is present as well as update HTTPS connections to use
TLS.
2015-07-07 23:04:16 -04:00
Ryan Kraus
d8d92e3ff7 merge isy_dev with balloob/dev 2015-07-07 22:49:07 -04:00
Michaël Arnauts
8b7a406fe7 Use xy_color instead of color in the configuration example 2015-07-07 22:33:40 +02:00
Paulus Schoutsen
940b2998ea Add REQUIREMENTS list to components 2015-07-07 00:01:46 -07:00
Paulus Schoutsen
aeae7c2c02 Reorganize util methods 2015-07-07 00:01:17 -07:00
Paulus Schoutsen
b346f6e8ad Allow installing component dependencies on the fly 2015-07-07 00:00:21 -07:00
Paulus Schoutsen
90739c9df9 Auto install core dependencies on boot 2015-07-06 23:59:21 -07:00
Paulus Schoutsen
4be9519e76 Update gitignore for pyvenv artifacts 2015-07-06 23:33:35 -07:00
Paulus Schoutsen
5550c89a86 Another frontend color picker fix 2015-07-06 10:07:12 -07:00
Paulus Schoutsen
820fd55249 Frontend bugfix: hide color picker when light is off 2015-07-06 08:11:23 -07:00
Paulus Schoutsen
4d81953562 Frontend: clean up code 2015-07-06 00:30:36 -07:00
Paulus Schoutsen
56c5d28ede Frontend: disable selecting text in cards/sidebar/header
Fixes #200
2015-07-06 00:29:15 -07:00
Paulus Schoutsen
65a74f68d5 Frontend: migrate to Polymer-based color picker 2015-07-06 00:28:22 -07:00
Ryan Kraus
90392ec303 Added app name display to Chromecast component. 2015-07-03 03:13:10 -04:00
Paulus Schoutsen
9cfefb64dd Rip out paper-menu because it is broken 2015-06-30 22:55:12 -07:00
Paulus Schoutsen
d4e9f26983 Clear password from input when logging out 2015-06-30 22:54:52 -07:00
Paulus Schoutsen
5316762a64 Frontend: Improve sidebar reliability on touch devices 2015-06-28 23:25:59 -07:00
Paulus Schoutsen
7a7ede22ea Frontend: extract voice command status component 2015-06-28 20:10:35 -07:00
Paulus Schoutsen
d719a09a58 Merge pull request #197 from theolind/dev
Added pyserial to requirements, needed by mysensors serial gateway
2015-06-28 18:48:44 -07:00
theolind
8d652ff34d Added pyserial to requirements, needed by mysensors serial gateway 2015-06-28 11:07:22 +02:00
Fabian Affolter
0fcc7d2b23 Merge branch 'dev' of github.com:fabaff/home-assistant into dev 2015-06-27 22:37:37 +02:00
Fabian Affolter
a34742040c remove unused stuff and update the names (same as in owm sensor) 2015-06-27 15:09:30 +02:00
Fabian Affolter
7f0c334391 Merge pull request #191 from fabaff/arduino
Arduino component
2015-06-27 13:14:29 +02:00
Fabian Affolter
694f9ec8e2 Merge branch 'dev' of github.com:fabaff/home-assistant into dev 2015-06-27 10:12:46 +02:00
Fabian Affolter
db5060b323 remove unused stuff and update the names (same as in owm sensor) 2015-06-27 10:11:37 +02:00
Fabian Affolter
f10c51b5f3 Merge pull request #192 from fabaff/owm-forecast
Forecast option for OpenWeatherMap sensor
2015-06-27 10:08:33 +02:00
Fabian Affolter
e971a01acd re-add unit 2015-06-27 09:59:05 +02:00
Fabian Affolter
0cd0d1ea97 another try (input from PyMata developer) 2015-06-25 15:54:33 +02:00
Fabian Affolter
84b6e499b3 Merge branch 'arduino' of github.com:fabaff/home-assistant into arduino 2015-06-25 13:13:42 +02:00
Fabian Affolter
169e7e9623 again requirements.txt 2015-06-25 13:12:15 +02:00
Fabian Affolter
ad1227d655 add pyserial 2015-06-25 13:11:22 +02:00
Fabian Affolter
636071a22a add arduino sensor platform 2015-06-25 13:11:22 +02:00
Fabian Affolter
20fd4ecb9a add arduino switch platform 2015-06-25 13:11:22 +02:00
Fabian Affolter
b33ae47a4c add arduino component 2015-06-25 13:11:22 +02:00
Fabian Affolter
0ebab8f612 add firmata bindings 2015-06-25 13:11:22 +02:00
Fabian Affolter
1eef88e85f add arduino component 2015-06-25 13:11:22 +02:00
Fabian Affolter
7b7e348837 Merge branch 'arduino' of github.com:fabaff/home-assistant into arduino 2015-06-25 11:43:28 +02:00
Fabian Affolter
34977b836a update pymata 2015-06-25 11:42:19 +02:00
Fabian Affolter
cda1374b49 add pyserial 2015-06-25 11:41:05 +02:00
Fabian Affolter
2c37fb639e add arduino sensor platform 2015-06-25 11:41:05 +02:00
Fabian Affolter
a01de8c90e add arduino switch platform 2015-06-25 11:41:05 +02:00
Fabian Affolter
a6f975d79f add arduino component 2015-06-25 11:41:05 +02:00
Fabian Affolter
457bd2339b add firmata bindings 2015-06-25 11:41:05 +02:00
Fabian Affolter
98a4f2fedc add arduino component 2015-06-25 11:41:05 +02:00
Paulus Schoutsen
5e79a8080b Frontend cleanup and bug fixes 2015-06-25 00:04:32 -07:00
Paulus Schoutsen
3f56b7e131 Frontend: oops, had some ES6 within Polymer 2015-06-24 14:12:28 -07:00
Paulus Schoutsen
b7b91f27db Frontend: fix bug where title was not shown for states partial 2015-06-23 23:43:14 -07:00
Paulus Schoutsen
ad15a14f5d Frontend: move hashchange/localstorage code into HA-JS 2015-06-23 23:22:32 -07:00
Paulus Schoutsen
2f876fb225 Merge remote-tracking branch 'origin/master' into dev 2015-06-23 20:44:19 -07:00
Paulus Schoutsen
2b9c0e637f Merge branch 'nuclear-js' into dev 2015-06-23 20:44:12 -07:00
Paulus Schoutsen
580adf8820 Frontend: reactor sidebar into own file 2015-06-23 20:43:59 -07:00
Fabian Affolter
892573e53e remove unused stuff and update the names (same as in owm sensor) 2015-06-23 12:34:55 +02:00
Fabian Affolter
fe600b7877 add forecast 2015-06-23 12:33:31 +02:00
Fabian Affolter
c4a0b41b8e update pyowm to 2.2.1 2015-06-23 08:23:53 +02:00
Paulus Schoutsen
d9e3c02df3 Update README.md 2015-06-22 15:03:02 -07:00
Paulus Schoutsen
8c2b6b5ca1 Merge pull request #189 from balloob/nuclear-js
Update frontend to use NuclearJS
2015-06-22 15:02:20 -07:00
Paulus Schoutsen
6f21f5f03e Merge pull request #190 from gitter-badger/gitter-badge
Add a Gitter chat badge to README.md
2015-06-22 15:02:14 -07:00
Fabian Affolter
1e4c401257 add pyserial 2015-06-22 19:00:38 +02:00
Fabian Affolter
8f8fdcdc82 add arduino sensor platform 2015-06-22 17:59:02 +02:00
Fabian Affolter
1d5a03a624 add arduino switch platform 2015-06-22 17:58:46 +02:00
Fabian Affolter
7a6a394bbf add arduino component 2015-06-22 17:58:27 +02:00
Fabian Affolter
c0721241e5 add firmata bindings 2015-06-22 17:57:49 +02:00
Fabian Affolter
8ce4635bd1 add arduino component 2015-06-22 17:57:09 +02:00
The Gitter Badger
aa44582575 Added Gitter badge 2015-06-22 07:49:23 +00:00
Paulus Schoutsen
49e9ae313c Merge pull request #186 from michaelarnauts/dev
Add TP-Link support for device_tracker
2015-06-22 00:34:17 -07:00
Paulus Schoutsen
ba7e252103 Merge pull request #188 from fabaff/file
File notification platform
2015-06-22 00:34:04 -07:00
Paulus Schoutsen
7ef0dec185 Update frontend to use NuclearJS 2015-06-22 00:25:56 -07:00
Fabian Affolter
13dac91fa6 remove blank line 2015-06-20 23:41:24 +02:00
Fabian Affolter
0269e89148 Merge branch 'file' of github.com:fabaff/home-assistant into file 2015-06-20 23:10:57 +02:00
Fabian Affolter
ef40b94a87 use with open 2015-06-20 23:03:53 +02:00
Fabian Affolter
832e9a631e add file notification platform 2015-06-20 19:57:11 +02:00
Fabian Affolter
3a1bc715b7 add file.py 2015-06-20 19:57:11 +02:00
Fabian Affolter
2f1b12a6f1 add file notification platform 2015-06-20 19:57:10 +02:00
Fabian Affolter
4221eef428 use a list and update docs 2015-06-20 16:33:19 +02:00
Fabian Affolter
c800508f87 update docs 2015-06-20 16:22:00 +02:00
Fabian Affolter
3abb185e16 add file notification platform 2015-06-20 11:00:20 +02:00
Fabian Affolter
d7dcee737b add file.py 2015-06-19 22:22:06 +02:00
Fabian Affolter
68a4928fb1 add file notification platform 2015-06-19 22:22:06 +02:00
Michaël Arnauts
abea8a2ff4 disable pylint warning, use correct format for mac addresses 2015-06-19 20:58:01 +02:00
Paulus Schoutsen
c9892569c9 Merge branch 'pr/185' into dev
Conflicts:
	requirements.txt
2015-06-18 23:45:37 -07:00
Paulus Schoutsen
26fcb9395e Merge pull request #187 from fabaff/systemmonitor
Systemmonitor sensor
2015-06-18 23:37:20 -07:00
Paulus Schoutsen
5ec85be299 Merge pull request #183 from ettisan/kodi
Kodi
2015-06-18 20:53:46 -07:00
Wolfgang Ettlinger
2017228503 clear all data when kodi is off 2015-06-18 11:46:02 +02:00
Wolfgang Ettlinger
689255dec0 fix detection of when Kodi is off/unreachable 2015-06-18 11:42:35 +02:00
Fabian Affolter
cdb1677b59 fix pylint issue 2015-06-17 23:58:14 +02:00
Fabian Affolter
cb35363e10 add missing resource 2015-06-17 23:46:48 +02:00
Fabian Affolter
05b70825fa add a couple of new resources 2015-06-17 23:42:11 +02:00
Michaël Arnauts
e5147235cc More code style fixes 2015-06-17 23:40:58 +02:00
Michaël Arnauts
c77dbaa67b Fix code guidelines 2015-06-17 23:32:33 +02:00
Michaël Arnauts
ac73c4db0f Add TP-Link support for device_tracker 2015-06-17 22:55:03 +02:00
Fabian Affolter
88923a8b18 update psutil to 3.0.0 2015-06-17 22:44:53 +02:00
Fabian Affolter
9b4b76d364 fix return value 2015-06-17 22:37:19 +02:00
Fabian Affolter
91b611acb7 fix return value 2015-06-17 22:36:54 +02:00
Fabian Affolter
7836cb2f01 add forecast.io sensor 2015-06-17 21:59:38 +02:00
Fabian Affolter
a31f9cd26a add forecast.py 2015-06-17 21:59:19 +02:00
Fabian Affolter
4a053ebda9 add python-forecastio 2015-06-17 21:58:23 +02:00
Wolfgang Ettlinger
4355686bd6 final draft of kodi module 2015-06-17 17:12:15 +02:00
Wolfgang Ettlinger
61638e8b72 Merge branch 'dev' of github.com:balloob/home-assistant into dev
Conflicts:
	requirements.txt
2015-06-17 13:45:59 +02:00
Wolfgang Ettlinger
cf07939792 first draft of kodi plugin 2015-06-17 13:44:39 +02:00
Paulus Schoutsen
bc6def277e Another fix for auto closing more info dialogs 2015-06-16 23:11:42 -07:00
Paulus Schoutsen
487c9e1e72 Update demo device tracker pictures 2015-06-16 22:49:41 -07:00
Paulus Schoutsen
8bb2ba2181 Bugfix for history that does not span 24h 2015-06-16 00:53:36 -07:00
Paulus Schoutsen
9fd850bf36 Make Wink update state less aggressive 2015-06-16 00:21:30 -07:00
Paulus Schoutsen
768e4a011e Merge pull request #182 from balloob/history-day-picker
History: Add support to fetch specific days
2015-06-16 00:13:47 -07:00
Paulus Schoutsen
f26ac070d5 History: Add support to fetch specific days 2015-06-16 00:08:57 -07:00
Paulus Schoutsen
d34ecd1c25 Merge pull request #180 from balloob/logbook-browser
Logbook - pick day to browse
2015-06-15 00:29:20 -07:00
Paulus Schoutsen
3381fff6bd Frontend: logbook - allow changing displayed day 2015-06-15 00:24:40 -07:00
Paulus Schoutsen
8a14f46595 Add support to logbook component to browse days 2015-06-14 22:56:55 -07:00
Paulus Schoutsen
4b7f8ca39d Merge pull request #179 from balloob/refactor-light
Refactor basic light structure
2015-06-14 20:01:33 -07:00
Paulus Schoutsen
3a8310c0cc Merge pull request #177 from balloob/refactor-switch
Refactor switch
2015-06-14 20:01:28 -07:00
Paulus Schoutsen
d3320963c3 Refactor basic light structure 2015-06-13 16:42:09 -07:00
Paulus Schoutsen
3d4392ce63 Refactor basic switch structure 2015-06-13 14:56:20 -07:00
Paulus Schoutsen
187da3a743 Merge branch 'dev' 2015-06-13 09:52:25 -07:00
Paulus Schoutsen
0f2a50c62f Fix frontend bug that opens more info dialog after toggle tap 2015-06-13 09:52:03 -07:00
Fabian Affolter
9792089b57 fix typo 2015-06-11 22:40:03 +02:00
Fabian Affolter
047ab10dde add comment 2015-06-11 22:33:55 +02:00
Fabian Affolter
39ccd1b3a4 fix typo 2015-06-11 22:32:58 +02:00
Fabian Affolter
f066c63900 add config details 2015-06-11 22:29:01 +02:00
Fabian Affolter
dd05d6878c add config details 2015-06-11 22:29:01 +02:00
Paulus Schoutsen
73ed0cd5bf Merge branch 'dev' 2015-06-11 09:15:00 -07:00
Paulus Schoutsen
d35591c5f4 Upgrade to latest pychromecast 2015-06-11 09:14:51 -07:00
Paulus Schoutsen
5f336621c0 Merge pull request #174 from eagleamon/dev
adds a coloourful log output
2015-06-11 08:47:53 -07:00
eagleamon
bcb4766f95 adds a coloourful log output 2015-06-11 15:01:11 +02:00
Paulus Schoutsen
26de87951a Merge branch 'dev'
Conflicts:
	homeassistant/components/media_player/cast.py
	requirements.txt
2015-06-11 00:36:31 -07:00
Paulus Schoutsen
58f4ab4e67 Bugfix frontend: show no state message when no states 2015-06-11 00:31:12 -07:00
Paulus Schoutsen
cac1c0d415 Bugfix for media player supported media commands 2015-06-11 00:17:44 -07:00
Paulus Schoutsen
2ea195a5d2 Merge pull request #158 from balloob/refactor-media-player
Initial refactor media player
2015-06-10 23:59:53 -07:00
Paulus Schoutsen
0f4de88b92 Fix PyLint import issue 2015-06-10 23:59:37 -07:00
Paulus Schoutsen
492ea478e7 Merge remote-tracking branch 'origin/dev' into refactor-media-player
Conflicts:
	homeassistant/components/frontend/version.py
2015-06-10 23:53:18 -07:00
Paulus Schoutsen
ae847994fc MPD platform fixes 2015-06-10 23:51:38 -07:00
Paulus Schoutsen
801eabe598 Bugfixes for media player more info dialog 2015-06-10 23:51:33 -07:00
Paulus Schoutsen
ca373b5aa5 Frontend: Workaround for tap bug in paper-dialog 2015-06-09 20:53:50 -07:00
Paulus Schoutsen
6c1a309c40 Minor CSS fixes for frontend 2015-06-09 00:26:15 -07:00
Paulus Schoutsen
8e50f50a48 Merge branch 'dev' into refactor-media-player
Conflicts:
	homeassistant/components/frontend/version.py
2015-06-08 23:08:00 -07:00
Paulus Schoutsen
5008b25a2d Fix MPD media player support 2015-06-08 23:06:41 -07:00
Paulus Schoutsen
90919a66d9 Fix Cast media player support 2015-06-08 22:49:43 -07:00
Paulus Schoutsen
452b082c82 Update media player core 2015-06-08 22:49:32 -07:00
Paulus Schoutsen
0b52b8c470 Fix more info dialog opening on touch devices 2015-06-08 21:08:27 -07:00
Paulus Schoutsen
7fa7ef103f Merge pull request #168 from fabaff/syslog
Syslog notifications
2015-06-07 22:46:15 -07:00
jamespcole
7a4d40a8fd Fixed some linting errors 2015-06-05 23:04:52 +10:00
jamespcole
aaf0ca2105 Very simple IP Camera support 2015-06-05 22:51:29 +10:00
Fabian Affolter
378d3798fd sync with ha.io 2015-06-04 18:18:26 +02:00
Fabian Affolter
d61bad16bf add links 2015-06-04 16:43:53 +02:00
Fabian Affolter
992706c5c1 update icon section 2015-06-04 16:33:34 +02:00
Fabian Affolter
3c50fc63c1 minor changes and fixes 2015-06-04 16:26:52 +02:00
Fabian Affolter
b8fcfbd182 add details about the pull requests and a quick-start section 2015-06-04 15:57:25 +02:00
Fabian Affolter
26c56d277e update doc 2015-06-04 09:53:43 +02:00
Fabian Affolter
fa7fc3ca5d fix pylint issues 2015-06-04 09:50:37 +02:00
Fabian Affolter
c78e1519af add syslog notification platform 2015-06-04 09:44:28 +02:00
Fabian Affolter
91764cacd9 add syslog.py 2015-06-04 09:44:10 +02:00
Hans Bakker
66024e5059 Initial update of cast device to MediaPlayerDevice. Added media_seek command and service to MediaPlayerDevice. 2015-06-04 00:17:03 +02:00
Paulus Schoutsen
6f09c0ae18 Merge pull request #167 from fabaff/smtp
SMTP notifications
2015-06-03 12:53:58 -07:00
Fabian Affolter
f24cd4feed add smtp notification platform 2015-06-03 20:39:16 +02:00
Fabian Affolter
d5efe33944 add smtp.py 2015-06-03 20:38:51 +02:00
Paulus Schoutsen
7f788d6be1 Update media player more info 2015-06-02 23:36:37 -07:00
Paulus Schoutsen
8e6ccea085 Update media player state card 2015-06-02 22:58:10 -07:00
Paulus Schoutsen
4cc8b0bda5 Merge branch 'dev' into refactor-media-player 2015-06-02 22:44:03 -07:00
Paulus Schoutsen
e1c880cb22 New frontend build 2015-06-02 22:41:44 -07:00
Paulus Schoutsen
7ba0b03e6c Frontend: do not open more info when tapping toggle 2015-06-02 22:38:57 -07:00
Paulus Schoutsen
b1cdf48ca0 Frontend: fix state card content showing outside of card 2015-06-02 22:25:44 -07:00
Paulus Schoutsen
a330f4c130 Update apple touch icon 2015-06-02 22:19:30 -07:00
Paulus Schoutsen
644a3058de Fix device tracker deadlock after exception in scanner 2015-06-02 21:39:33 -07:00
Paulus Schoutsen
73dab5a398 Make customize parsing more robust 2015-06-02 21:31:50 -07:00
Paulus Schoutsen
23b851fab4 Merge branch 'pr/166' into dev
Conflicts:
	requirements.txt
2015-06-02 18:47:47 -07:00
Fabian Affolter
a6b4d539a6 add zwave and nest 2015-06-02 23:28:59 +02:00
Fabian Affolter
73139a76a8 add ISY994 and modbus 2015-06-02 23:22:27 +02:00
Fabian Affolter
bf1a6f5899 add other sensors 2015-06-02 23:10:12 +02:00
Fabian Affolter
718ff6b326 add notifications and mpd 2015-06-02 23:02:34 +02:00
Fabian Affolter
16be76a038 add missing bracket 2015-06-02 22:41:57 +02:00
Fabian Affolter
3d1743d91d fix period 2015-06-02 22:37:08 +02:00
Fabian Affolter
a695da88df separate links 2015-06-02 22:26:26 +02:00
Fabian Affolter
c75c123d37 fix first sentence 2015-06-02 22:14:30 +02:00
Paulus Schoutsen
a8b6aeeb05 Merge pull request #157 from fabaff/swiss-public-transport
Swiss public transport
2015-06-02 09:57:41 -07:00
Finbarr Brady
b5ee05a13e docs 2015-06-02 17:33:05 +01:00
Finbarr Brady
b11320a5fb Bump version 2015-06-02 17:18:41 +01:00
Finbarr Brady
3701a82de1 Fix typo 2015-06-02 17:17:36 +01:00
Finbarr Brady
ebc0ffd879 https false 2015-06-02 17:14:12 +01:00
Paulus Schoutsen
066d515547 Merge pull request #163 from ettisan/switchcmds2
Switch cmds
2015-06-02 09:09:44 -07:00
Finbarr Brady
bbf8420d80 Updated component id 2015-06-02 16:04:45 +01:00
Wolfgang Ettlinger
5d1e0d5c44 fixed pylint warnings 2015-06-02 17:04:14 +02:00
Finbarr Brady
bae530b30d Delete original file 2015-06-02 16:03:29 +01:00
Finbarr Brady
84e81a9e4e Renamed hikvision component. Added module from pip 2015-06-02 16:00:29 +01:00
Fabian Affolter
82518fbbf4 Merge branch 'swiss-public-transport' of github.com:fabaff/home-assistant into swiss-public-transport 2015-06-02 15:57:45 +02:00
Fabian Affolter
512c4629b6 implement comments from #157 2015-06-02 15:54:53 +02:00
Fabian Affolter
4292c1f207 Revert "add timestamp to short time"
This reverts commit 1be50d83dc.

Conflicts:
	homeassistant/util/dt.py
2015-06-02 15:54:53 +02:00
Fabian Affolter
84c7149f0f update journey 2015-06-02 15:54:53 +02:00
Fabian Affolter
2317114b04 switch from error to execption for logger 2015-06-02 15:54:53 +02:00
Fabian Affolter
284dbff2d5 use string formatting 2015-06-02 15:54:53 +02:00
Fabian Affolter
713a03ad89 update depenency handling (requests) 2015-06-02 15:54:53 +02:00
Fabian Affolter
57e2a8a0c9 remove lat/long check 2015-06-02 15:54:53 +02:00
Fabian Affolter
a50ed46950 add shortcut 2015-06-02 15:54:53 +02:00
Fabian Affolter
a3cdb667ba add swiss_public_transport.py 2015-06-02 15:54:53 +02:00
Fabian Affolter
a70c32da3c add newline 2015-06-02 15:54:53 +02:00
Fabian Affolter
45d67176c5 add newline at the end 2015-06-02 15:54:53 +02:00
Fabian Affolter
c0c92a82e2 add swiss public transport sensor 2015-06-02 15:54:52 +02:00
Fabian Affolter
ba8d429a9f add timestamp to short time 2015-06-02 15:54:52 +02:00
Wolfgang Ettlinger
b35bdc606a style adaptions 2015-06-02 15:54:43 +02:00
Fabian Affolter
458838e9c6 implement comments from #157 2015-06-02 15:49:24 +02:00
Wolfgang Ettlinger
d91f414313 added generic module to switch swiches using shell commands 2015-06-02 15:40:02 +02:00
Paulus Schoutsen
eae52a8faf More target temperature nest fixes 2015-06-02 00:02:50 -07:00
Paulus Schoutsen
1585a91aec Add idle state to const.py 2015-06-01 23:55:54 -07:00
Paulus Schoutsen
bacff3de8d Add demo TV show player 2015-06-01 23:53:36 -07:00
Paulus Schoutsen
5eaf3d40ad Add demo music player 2015-06-01 23:42:19 -07:00
Paulus Schoutsen
a7b79fc8b2 Initial refactor media player 2015-06-01 23:06:46 -07:00
Paulus Schoutsen
dbf2f6223c Merge branch 'dev' into pr/156
Conflicts:
	requirements.txt
2015-06-01 23:00:37 -07:00
Paulus Schoutsen
66a380dd8e Ensure generate_entity_id returns valid entity ids 2015-06-01 22:55:33 -07:00
Paulus Schoutsen
a557cf388b Better approach writing default configuration 2015-06-01 22:50:57 -07:00
Paulus Schoutsen
17af1a68e8 Fix for Nest thermostats with a range as target temperature 2015-06-01 22:00:41 -07:00
Paulus Schoutsen
e56fccaefe Merge pull request #155 from fabaff/music-player-demon
Add Music player demon
2015-06-01 14:52:29 -07:00
wind-rider
e9c0cb74d8 Merge pull request #3 from balloob/media-player-muting
Volume muting fixes
2015-06-01 20:51:45 +02:00
Hans Bakker
a3f2f7c039 remove console.log 2015-06-01 20:51:21 +02:00
Fabian Affolter
c8111b988e Revert "add timestamp to short time"
This reverts commit 1be50d83dc.

Conflicts:
	homeassistant/util/dt.py
2015-06-01 14:02:27 +02:00
Fabian Affolter
1e5e06fef5 update journey 2015-06-01 13:51:00 +02:00
Fabian Affolter
90d55ef901 switch from error to execption for logger 2015-06-01 13:49:46 +02:00
Fabian Affolter
9d1e881f12 use string formatting 2015-06-01 13:49:08 +02:00
Fabian Affolter
da68e4ab11 update depenency handling (requests) 2015-06-01 13:47:32 +02:00
Fabian Affolter
1aa98bea2b remove lat/long check 2015-06-01 13:40:21 +02:00
Fabian Affolter
038bfde6aa add shortcut 2015-06-01 13:37:57 +02:00
Fabian Affolter
fb63198688 fix return values, disconnect from mpd, and fix pylint issue 2015-06-01 13:25:55 +02:00
Paulus Schoutsen
3b43fe7431 Fix PyLint issues 2015-05-31 22:13:02 -07:00
Paulus Schoutsen
15c3e2f516 Volume muting fixes 2015-05-31 21:07:58 -07:00
Hans Bakker
21cf7ceb07 minor update to more info card 2015-06-01 00:51:17 +02:00
Hans Bakker
321e821ae8 Update requirements.txt to include mute-capable pychromecast 2015-06-01 00:08:39 +02:00
Fabian Affolter
c8c3b825e4 add swiss_public_transport.py 2015-05-31 23:31:24 +02:00
Fabian Affolter
fafea688e4 add newline 2015-05-31 23:25:38 +02:00
Fabian Affolter
b1b0b2bc65 add newline at the end 2015-05-31 22:55:47 +02:00
Fabian Affolter
a6b7f47d74 add swiss public transport sensor 2015-05-31 22:55:02 +02:00
Fabian Affolter
1be50d83dc add timestamp to short time 2015-05-31 22:54:16 +02:00
Hans Bakker
938200478f Merge remote-tracking branch 'upstream/dev' into dev 2015-05-31 22:30:05 +02:00
Hans Bakker
81245495ae Add volume/mute button instead of text label, depends on new pychromecast method. Also I made the power button the only accented button. 2015-05-31 22:27:59 +02:00
Fabian Affolter
26c8d81dbf add configuration desc 2015-05-31 22:21:11 +02:00
Fabian Affolter
ab4ed2e032 add check 2015-05-31 20:53:19 +02:00
Fabian Affolter
cb9fe8d9ff update format 2015-05-31 20:53:18 +02:00
Fabian Affolter
bd561dea6e add mpd 2015-05-31 20:53:18 +02:00
Fabian Affolter
88523e6ffb add mpd bindings 2015-05-31 20:53:18 +02:00
Fabian Affolter
4a25b59d33 add period 2015-05-31 20:53:18 +02:00
Fabian Affolter
453b5a3e8c add initial mpd 2015-05-31 20:53:18 +02:00
Fabian Affolter
996322a2d3 add date 2015-05-31 20:53:18 +02:00
Paulus Schoutsen
9efc357226 Merge pull request #152 from wind-rider/dev
Initial commit for volume slider in media player more info card
2015-05-31 11:47:11 -07:00
wind-rider
5c48c87af0 Merge pull request #2 from balloob/media-player-volume
Fixes for Chromecast volume
2015-05-31 20:45:22 +02:00
Paulus Schoutsen
ed46a93848 Fixes for Chromecast volume
It was line 119 from cast.py that was driving me crazy
2015-05-31 11:37:54 -07:00
Paulus Schoutsen
3c31758826 Merge pull request #153 from fbradyirl/transmission-turtle
Transmission turtle
2015-05-31 08:41:52 -07:00
Finbarr Brady
bc8aa82a13 Removing toggle function as it is confusing things more than it should 2015-05-31 12:00:30 +00:00
Finbarr Brady
c1b62bf672 Only toggle if in the correct state. 2015-05-31 11:51:45 +00:00
Hans Bakker
c445563ab1 Fix pylint 2015-05-31 13:47:40 +02:00
Hans Bakker
b30bdbfc6a Initial commit for volume slider in media player more info card 2015-05-31 13:36:28 +02:00
Paulus Schoutsen
0583d63b67 Fix Cast platform detecting it was on while it was not 2015-05-31 01:29:28 -07:00
Paulus Schoutsen
afd99a0c6c Media Player and cast improvements 2015-05-31 00:38:14 -07:00
Paulus Schoutsen
678be46bb9 Add more info card for media player 2015-05-30 14:02:46 -07:00
Paulus Schoutsen
197611fe8e Upgrade cast entity from polling to pushing state 2015-05-30 12:35:35 -07:00
Paulus Schoutsen
1e7e9a9e02 Add a state card for media player 2015-05-30 12:08:35 -07:00
Paulus Schoutsen
e74806ab70 Fix Google Cast discovery 2015-05-30 11:41:39 -07:00
Paulus Schoutsen
10d74bb37e Add turn_on method to media_player 2015-05-30 11:29:49 -07:00
Paulus Schoutsen
6d125a8dfb Update missing dependency code for Cast 2015-05-30 10:54:27 -07:00
Paulus Schoutsen
bbf3bbcd4b Merge pull request #149 from fbradyirl/transmission-turtle
New Transmission turtle mode switch. Using existing TransmissionRpc r…
2015-05-30 07:48:35 -07:00
Paulus Schoutsen
43f89209d3 Merge pull request #148 from fbradyirl/luci-fix
Fix for getting device names using Luci. Tested on Barier Breaker.
2015-05-30 07:46:40 -07:00
Finbarr Brady
ecd09f22ef Support for Hikvision camera motion detection. 2015-05-30 13:44:29 +00:00
Finbarr Brady
1c55878271 New Transmission turtle mode switch. Using existing TransmissionRpc requirement. 2015-05-30 13:34:23 +00:00
Finbarr Brady
f110dc970d Fix for getting device names using Luci. Tested on Barier Breaker. 2015-05-30 13:30:34 +00:00
Paulus Schoutsen
96edd759a8 media_player.cast: support thumbnail + title 2015-05-30 00:53:29 -07:00
Paulus Schoutsen
f1843a57e0 media_player.cast: support thumbnail + title 2015-05-30 00:52:33 -07:00
Paulus Schoutsen
ef0eb8be02 Merge remote-tracking branch 'origin/master' into dev 2015-05-30 00:36:10 -07:00
Paulus Schoutsen
b332a9ed41 Merge pull request #147 from wind-rider/pychromecast-fixing
Make chromecast component work again
2015-05-30 00:32:23 -07:00
wind-rider
b011d14653 Merge pull request #1 from balloob/pychromecast-fixing-fixing
Update cast integration
2015-05-30 09:21:57 +02:00
Paulus Schoutsen
b50216600b Update cast integration 2015-05-29 22:36:40 -07:00
Paulus Schoutsen
5ccb7a5856 Merge pull request #132 from balloob/polymer-9
Upgrade to Polymer 1.0
2015-05-29 21:16:48 -07:00
Paulus Schoutsen
124d50e6d6 Polymer 1.0: Update to latest version 2015-05-29 20:26:07 -07:00
Paulus Schoutsen
c91b2cc795 Polymer 1.0: Fix layout issue in logbook 2015-05-29 20:05:05 -07:00
Paulus Schoutsen
a53265b76e Polymer 1.0: Fix listening bar in states partial 2015-05-29 17:57:52 -07:00
Hans Bakker
ac8a2d03d8 Fix play_pause command 2015-05-29 22:31:52 +02:00
Hans Bakker
3e01ce2a6c Proposed fixes to chromecast component 2015-05-29 22:19:42 +02:00
Paulus Schoutsen
24b575d21a Polymer 1.0: Update build script and add new build 2015-05-28 01:08:50 -07:00
Paulus Schoutsen
5367ac562c Polymer 1.0: Migrate to use paper-styles for font 2015-05-28 00:50:52 -07:00
Paulus Schoutsen
d503ee94f5 Polymer 1.0: Clean up 2015-05-28 00:42:26 -07:00
Paulus Schoutsen
63114b988f Polymer 1.0: Fix more info dialog closing on toggling entity 2015-05-28 00:02:18 -07:00
Paulus Schoutsen
69630a53f7 Polymer 1.0: Fix dialog appearance on small screens 2015-05-27 23:51:17 -07:00
Paulus Schoutsen
ce7e6d37ed Polymer 1.0: more info content style fixes 2015-05-27 23:43:00 -07:00
Paulus Schoutsen
ab7536ffd7 Polymer 1.0: style fixes 2015-05-27 23:30:22 -07:00
Paulus Schoutsen
2ce090e4b3 Polymer 1.0: more info light - reposition post transition 2015-05-27 22:48:15 -07:00
Paulus Schoutsen
986b843e0d Polymer 1.0: Remove unused styles 2015-05-27 22:47:50 -07:00
Paulus Schoutsen
26837ae8d3 Polymer 1.0: Bugfix - menu button works again 2015-05-27 22:47:37 -07:00
Paulus Schoutsen
1cdd5db29f Polymer 1.0: Clean up state badge 2015-05-27 22:47:11 -07:00
Paulus Schoutsen
cf7a1392ac Polymer 1.0: Use paper scroll header panel for partial states 2015-05-27 22:46:52 -07:00
Paulus Schoutsen
b0fdd45205 Polymer 1.0: Move attribute classnames logic to more info elements 2015-05-27 22:04:38 -07:00
Paulus Schoutsen
57d5022e3d Polymer 1.0: dev tools - fire event button now works 2015-05-27 22:03:24 -07:00
Paulus Schoutsen
a7f9a5c4ad Polymer 1.0: More info dialog bug fixes 2015-05-27 22:02:58 -07:00
Paulus Schoutsen
8b1d3c7658 Polymer 1.0 :-) 2015-05-27 22:02:36 -07:00
Paulus Schoutsen
95f0be6247 Merge pull request #144 from Dutchy-/track_new_devices
Add a configuration option to track new devices by default.
2015-05-27 00:45:48 -07:00
Paulus Schoutsen
99ccfc241c Merge pull request #143 from Dutchy-/master
Let the nmap device tracker return upper case MAC addresses.
2015-05-27 00:45:22 -07:00
Paulus Schoutsen
a8aa5b1447 Polymer .9: Convert more info contents 2015-05-27 00:44:47 -07:00
Paulus Schoutsen
e5f0e57980 Polymer .9: Fix history in more info dialogs 2015-05-26 23:32:11 -07:00
Fabian Affolter
531b702d65 fix typo 2015-05-26 17:02:38 +02:00
Edwin Smulders
41ec85053e Add a configuration option to track new devices by default. 2015-05-26 00:01:16 +02:00
Edwin Smulders
71ac550e7d Let the nmap device tracker return upper case MAC addresses. 2015-05-25 21:33:58 +02:00
Paulus Schoutsen
852305b996 Merge pull request #138 from fabaff/rewrite-owm
OpenWeatherMap sensor
2015-05-22 07:05:44 -07:00
Fabian Affolter
381d160e3b add throttle 2015-05-22 10:11:21 +02:00
Paulus Schoutsen
eb0584d466 Polymer .9: Style fixes 2015-05-22 00:21:47 -07:00
Paulus Schoutsen
76cf0e445d Polymer .9: Style fixes 2015-05-21 22:47:17 -07:00
Fabian Affolter
1d63e5a2df switch to list 2015-05-21 19:05:31 +02:00
Fabian Affolter
f9a8575414 fix typo 2015-05-21 18:43:38 +02:00
Fabian Affolter
3939d4e2f0 fix statement 2015-05-21 10:31:42 +02:00
Paulus Schoutsen
451cde7918 Polymer .9: Some cleanup 2015-05-20 23:42:46 -07:00
Paulus Schoutsen
69ae1d3534 Polymer .9: More info dialog with default content 2015-05-20 23:41:54 -07:00
Paulus Schoutsen
de243855c4 Polymer .9: Notifications 2015-05-20 22:41:58 -07:00
Paulus Schoutsen
9c71970d69 Merge pull request #128 from fabaff/bitcoin
Bitcoin sensor
2015-05-20 15:03:44 -07:00
Paulus Schoutsen
4f5ad3c7b6 Merge pull request #136 from balloob/master
Add LimitlessLED to dev branch
2015-05-20 14:28:35 -07:00
Fabian Affolter
2d1b934a1c minor updates 2015-05-20 18:30:57 +02:00
Fabian Affolter
9392f9b512 use throttle as balloob suggested 2015-05-20 18:30:57 +02:00
Fabian Affolter
397336e03c switch to list for options 2015-05-20 18:30:57 +02:00
Fabian Affolter
61148d8215 add throttle, rename variable, fix pylint issues 2015-05-20 18:30:57 +02:00
Fabian Affolter
4bed8647ac add bitcoin sensor 2015-05-20 18:30:57 +02:00
Fabian Affolter
022f6608b9 add blockchain as requirement 2015-05-20 18:30:57 +02:00
Fabian Affolter
827fd4d070 add bitcoin sensor 2015-05-20 18:30:57 +02:00
Alfie "Azelphur" Day
2b444b0a28 And fix the part where I'm being stupid 2015-05-20 18:30:57 +02:00
Alfie "Azelphur" Day
f52412df41 More pylint fixes 2015-05-20 18:30:56 +02:00
Alfie "Azelphur" Day
0e9dc61805 Fix some code styling violations 2015-05-20 18:30:56 +02:00
Alfie "Azelphur" Day
6132ed1967 Add ledcontroller to requirements.txt 2015-05-20 18:30:56 +02:00
Alfie "Azelphur" Day
5b1c372fdf Add configuration.yaml example in the header 2015-05-20 18:30:56 +02:00
Alfie "Azelphur" Day
c97ab456f2 Add basic support for LimitlessLED 2015-05-20 18:30:56 +02:00
Paulus Schoutsen
0466ae5e07 Merge pull request #134 from jamespcole/http-sessions
Added basic http sessions for authentication
2015-05-20 07:52:54 -07:00
jamespcole
26987148b5 Merge remote-tracking branch 'upstream/dev' into http-sessions
Merged in the latest upstream changes
2015-05-20 17:14:01 +10:00
jamespcole
5606d4bb12 fixed session error during automated tests 2015-05-20 17:13:13 +10:00
Paulus Schoutsen
5baf16ad6e Polymer .9: convert dev tools 2015-05-19 23:38:32 -07:00
jamespcole
a8e7903f39 refactored the session store into a separate class 2015-05-19 19:18:41 +10:00
Paulus Schoutsen
33122701b9 Polymer .9: Convert History 2015-05-18 23:49:48 -07:00
Paulus Schoutsen
f517445d36 Polymer .9: Remove workaround in sidebar 2015-05-18 18:43:32 -07:00
Paulus Schoutsen
9d41958b3a Merge pull request #135 from Azelphur/master
Add basic support for LimitlessLED
2015-05-18 18:40:35 -07:00
Alfie "Azelphur" Day
7087c20c4f And fix the part where I'm being stupid 2015-05-19 02:33:37 +01:00
Alfie "Azelphur" Day
cb54fb5a64 More pylint fixes 2015-05-19 02:31:30 +01:00
Alfie "Azelphur" Day
e2b08a1758 Fix some code styling violations 2015-05-19 02:09:34 +01:00
Alfie "Azelphur" Day
c43e014304 Add ledcontroller to requirements.txt 2015-05-19 01:58:16 +01:00
Alfie "Azelphur" Day
9c7f1d94c0 Add configuration.yaml example in the header 2015-05-19 01:55:37 +01:00
Alfie "Azelphur" Day
a86852fe90 Add basic support for LimitlessLED 2015-05-19 00:27:09 +01:00
jamespcole
80f0c42844 Refactored session handling into a separate class 2015-05-19 03:57:35 +10:00
James Cole
0a78b02bf1 Merge pull request #133 from jamespcole/vera-fixes
Vera fixes for UI7 firmware update
2015-05-19 03:09:46 +10:00
jamespcole
dba9f8854f Removed conversion to local time from UTC for last_tripped_time for vera sensors and switches 2015-05-19 00:31:59 +10:00
jamespcole
8431fd822f Fixed flake8 blank line error 2015-05-19 00:08:02 +10:00
jamespcole
be0f11b1a5 Updated example config with new session config option 2015-05-18 23:56:22 +10:00
jamespcole
721dc6dae4 Addd basic http sessions to the http component 2015-05-18 23:54:32 +10:00
jamespcole
39dee9d17c Merge branch 'dev-clean' into vera-fixes
Merged with upstream changes
2015-05-18 19:27:21 +10:00
jamespcole
1d7d0ea2d4 updated vera submodule to the latest version for the UI7 formware update 2015-05-18 19:18:53 +10:00
jamespcole
1c47ade641 Updated with new switch category for UI7 firmware update and fixed excpetion when trippable sensor has never been tripped 2015-05-18 18:54:25 +10:00
Paulus Schoutsen
52895658e5 Polymer .9: convert logbook 2015-05-18 00:16:57 -07:00
Paulus Schoutsen
c3645e463e Polymer .9: convert state cards 2015-05-17 23:09:25 -07:00
Paulus Schoutsen
4576a1d245 Polymer .9: show basic state cards 2015-05-17 22:08:42 -07:00
Paulus Schoutsen
313eb71b17 Polymer .9: sidebar 2015-05-17 13:31:48 -07:00
Paulus Schoutsen
8f51741c65 Merge pull request #131 from andythigpen/yaml-include
Fix issues with YAML include tag.
2015-05-16 11:45:42 -07:00
Andrew Thigpen
fc560d179e Fix issues with YAML include tag.
Calling load_yaml_config_file(config_path) causes issues when trying to
load from multiple files in a directory since config_path is modified in
the closure.

This also separates the parse code block into its own function so that
there is no restriction that the object be a dictionary for included
files.  This means that scalars, lists, etc. can also be stored in
separate files -- for example, the scene component expects a list.
2015-05-16 11:52:27 -05:00
Paulus Schoutsen
8d6bbb8c1a Polymer .9: foundation + login form 2015-05-16 02:24:06 -07:00
Paulus Schoutsen
e0d697d0bc Default to UTC if invalid timezone specified 2015-05-15 23:28:16 -07:00
Paulus Schoutsen
f3f2240e4a Remove usage of deprecated hass.local_api in __main__ 2015-05-15 23:28:16 -07:00
Paulus Schoutsen
e3e64a3a18 Merge pull request #130 from andythigpen/yaml-include
Add include YAML tag.
2015-05-15 22:34:50 -07:00
Andrew Thigpen
6ef60aa979 Add include YAML tag.
Allows including other files in the main configuration.yaml file.  With
this functionality, passwords or other sensitive information can be
stored in a separate file from the main configuration.
2015-05-15 20:10:30 -05:00
Paulus Schoutsen
00664ac601 Merge pull request #121 from balloob/dev
Update master with latest changes
2015-05-14 22:50:22 -07:00
Paulus Schoutsen
a4e0a7f235 Fix platform discovery not working 2015-05-14 21:36:12 -07:00
Paulus Schoutsen
ae0cf49560 UTC upgrades for scheduler, nmap tracker, dsl_trigger 2015-05-14 21:07:15 -07:00
Paulus Schoutsen
cd2b9ce975 Merge pull request #127 from andythigpen/mysensors-config
Update mysensors component to read debug, persistence values.
2015-05-14 17:30:31 -07:00
Paulus Schoutsen
3f8fd8aa09 Merge pull request #126 from andythigpen/script-utc
Fix UTC issue with script component.
2015-05-14 17:30:26 -07:00
Andrew Thigpen
8b8cb28259 Update mysensors component to read debug, persistence values. 2015-05-14 19:02:10 -05:00
Andrew Thigpen
ef138bb132 Fix utc issue with script component. 2015-05-14 18:54:13 -05:00
Fabian Affolter
fdb46d80ba Update documentation 2015-05-13 19:06:17 -07:00
Paulus Schoutsen
6b0e29ad55 Update git submodules 2015-05-13 00:14:50 -07:00
Paulus Schoutsen
8248dfa004 Merge pull request #122 from fabaff/beat
Add beat and utc to time_date sensor
2015-05-12 23:40:45 -07:00
Fabian Affolter
80f2ac5c3c add title to header 2015-05-13 08:17:38 +02:00
Fabian Affolter
7eaa8a3f16 fix typo 2015-05-13 08:15:53 +02:00
Fabian Affolter
1776430f6b fix typo 2015-05-13 08:14:00 +02:00
Fabian Affolter
584d3ea272 add utc and beat, use more appropriate variable names 2015-05-12 20:28:04 +02:00
Paulus Schoutsen
52f5c45ca8 Merge pull request #120 from balloob/chore-platform-dependencies
Allow platforms to specify dependencies.
2015-05-12 07:47:41 -07:00
Paulus Schoutsen
c523a0b509 Improve error message when prepare_setup_platform fails. 2015-05-11 22:29:53 -07:00
Paulus Schoutsen
4eeaa16f16 Convert some double to single quotes. 2015-05-11 22:23:38 -07:00
Paulus Schoutsen
e630476f9f Allow platforms to specify dependencies 2015-05-11 22:23:20 -07:00
Paulus Schoutsen
d7ab76106d Merge pull request #119 from balloob/feature-recorder-states-store-eventid
Recorder saves which event caused state change
2015-05-11 19:56:18 -07:00
Fabian Affolter
44045a02f2 update header 2015-05-11 18:06:25 +02:00
Fabian Affolter
6b42227b13 update header 2015-05-11 18:06:12 +02:00
Fabian Affolter
61e1f56922 update header 2015-05-11 18:05:58 +02:00
Fabian Affolter
2539c93783 update header 2015-05-11 18:05:46 +02:00
Fabian Affolter
8f5a9859c3 update header 2015-05-11 18:05:35 +02:00
Fabian Affolter
21e2898868 fix typo 2015-05-11 17:27:25 +02:00
Fabian Affolter
69cc3c8261 add ddwrt 2015-05-11 17:26:18 +02:00
Paulus Schoutsen
df18acbabb Recorder saves which event caused state change 2015-05-10 23:41:42 -07:00
Paulus Schoutsen
c719e7dc31 Allow config.path to take multiple arguments 2015-05-10 23:05:37 -07:00
Paulus Schoutsen
0a28f3f648 Merge pull request #115 from fabaff/xmpp
Xmpp notifications
2015-05-10 22:57:41 -07:00
Fabian Affolter
67ca009690 update header 2015-05-10 23:49:09 +02:00
Fabian Affolter
ec0dd39220 update header 2015-05-10 23:45:47 +02:00
Fabian Affolter
379171e302 update header 2015-05-10 23:44:27 +02:00
Fabian Affolter
24499d7bed update header 2015-05-10 23:43:37 +02:00
Fabian Affolter
2ab1ce7b83 update title in header 2015-05-10 23:27:22 +02:00
Fabian Affolter
76fd70657a remove deprecated parameter 2015-05-10 23:22:06 +02:00
Fabian Affolter
f103d5964e remove deps of sleekxmpp 2015-05-10 23:20:36 +02:00
Fabian Affolter
7b0765e064 add xmpp.py 2015-05-10 22:23:25 +02:00
Fabian Affolter
07b5f3d597 initial xmpp nofication platform 2015-05-10 22:23:25 +02:00
Fabian Affolter
0e9685e55f add requirements for xmpp notifications 2015-05-10 22:23:25 +02:00
Paulus Schoutsen
b6c88b1a6c Merge pull request #116 from fabaff/time-date
Time date sensor
2015-05-08 13:51:58 -07:00
Fabian Affolter
9f042db0f5 call dt_util.now() only once, add additional format 2015-05-08 18:50:57 +02:00
Fabian Affolter
03993cd5fa fix if and remove tz 2015-05-08 18:39:28 +02:00
Fabian Affolter
079ec43291 remove list 2015-05-08 18:31:48 +02:00
Fabian Affolter
b55e8fc4c4 add time_date.py 2015-05-08 17:44:27 +02:00
Fabian Affolter
f72cedf446 add missing space 2015-05-08 17:03:42 +02:00
Fabian Affolter
5266e10140 add new methods for date/time output 2015-05-08 17:00:12 +02:00
Fabian Affolter
503a2adc38 add date & time sensor 2015-05-08 16:59:46 +02:00
Paulus Schoutsen
dfc50562a1 Update netdisco to latest version 2015-05-04 00:35:36 -07:00
Paulus Schoutsen
5452221cd2 Frontend bugfixes 2015-05-01 22:54:59 -07:00
Paulus Schoutsen
972743cb63 Fix style issue 2015-05-01 20:56:10 -07:00
Paulus Schoutsen
916c30072b Update frontend dependencies and compile new version 2015-05-01 20:49:02 -07:00
Paulus Schoutsen
30f78f7fa6 Add API endpoint to bootstrap frontend 2015-05-01 19:02:29 -07:00
Paulus Schoutsen
ca8be5015a Add config API endpoint 2015-05-01 18:24:32 -07:00
Paulus Schoutsen
e07fbbbcd8 Merge pull request #114 from fabaff/weather
OpenWeatherMap sensor
2015-05-01 14:16:47 -07:00
Fabian Affolter
fd6de36d2c add openweathermap, sort the content alphabetically 2015-05-01 21:56:42 +02:00
Fabian Affolter
70f1ec9dce add openweathermap sensor 2015-05-01 21:52:34 +02:00
Fabian Affolter
4001a2d316 add pyown 2015-05-01 17:05:46 +02:00
Paulus Schoutsen
20bdc82cba Do not show history in more info if component not loaded 2015-05-01 06:14:38 -07:00
Paulus Schoutsen
2196f77081 Exclude __main__ from coveragerc 2015-04-30 22:44:41 -07:00
Paulus Schoutsen
09f1983d40 Update dependency check in __main__ 2015-04-30 22:44:24 -07:00
Paulus Schoutsen
a752bc012c Merge pull request #113 from balloob/refactor-utc
Refactor to use UTC internally
2015-04-30 22:07:43 -07:00
Paulus Schoutsen
29e376a66e Assign unique ports for history and logbook tests 2015-04-30 21:59:24 -07:00
Paulus Schoutsen
2cad421de9 Add tests for logbook component 2015-04-30 21:53:10 -07:00
Paulus Schoutsen
17cc9a43bb Add tests for history component 2015-04-30 21:03:01 -07:00
Paulus Schoutsen
cf5278b7e9 Add tests for recorder component 2015-04-29 23:21:31 -07:00
Paulus Schoutsen
fd67e47128 Add tests for device_sun_light_trigger 2015-04-29 22:26:54 -07:00
Paulus Schoutsen
ef470de3ca Update coveragerc to hide latest device components 2015-04-29 22:25:19 -07:00
Paulus Schoutsen
5105c5f200 Update coveragerc to hide latest device components 2015-04-29 09:01:27 -07:00
Paulus Schoutsen
040dd3c409 Strip microseconds on state.last_updated 2015-04-29 08:18:53 -07:00
Paulus Schoutsen
10a5db7924 UTC bugfix for more-info-sun 2015-04-28 23:50:53 -07:00
Paulus Schoutsen
3720333927 UTC bugfixes 2015-04-28 22:38:43 -07:00
Paulus Schoutsen
54904a1630 Make frontend UTC aware 2015-04-28 22:34:44 -07:00
Paulus Schoutsen
e0ecb64a10 Use UTC as the internal datetime format 2015-04-28 19:12:05 -07:00
Paulus Schoutsen
847d049f1c Merge pull request #109 from fabaff/instapush
Instapush notification
2015-04-28 07:04:38 -07:00
Fabian Affolter
234669f469 fix format 2015-04-28 09:43:53 +02:00
Paulus Schoutsen
7a529aee63 Merge pull request #112 from fabaff/nma
Nma notifications
2015-04-27 23:56:14 -07:00
Fabian Affolter
14d0c1325c fix format 2015-04-28 08:45:40 +02:00
Fabian Affolter
b76b002966 try to pass travis build 2015-04-27 23:51:59 +02:00
Fabian Affolter
8b64e905b8 fix pylint error 2015-04-27 23:34:17 +02:00
Fabian Affolter
5ca1a80ad5 fix pylint errors 2015-04-27 19:47:27 +02:00
Fabian Affolter
94e004a828 rework to include checks on boot 2015-04-27 19:34:34 +02:00
Fabian Affolter
69799bd11e minor layout changes 2015-04-27 14:55:51 +02:00
Fabian Affolter
4a5c32e375 no longer use pynma and check if keys are ok 2015-04-27 14:52:22 +02:00
andythigpen
362e176397 Merge pull request #89 from andythigpen/mysensors
MySensors support
2015-04-26 13:03:31 -05:00
Fabian Affolter
7593ecb870 initial instapush notification 2015-04-26 19:48:26 +02:00
Paulus Schoutsen
cb7b0d8a4f Merge pull request #108 from balloob/chore-improve-config
Refactor config files into separate module
2015-04-26 10:31:59 -07:00
Paulus Schoutsen
ad016de653 Move config code to separate module + test cover it 2015-04-26 10:05:01 -07:00
Paulus Schoutsen
5e6a502167 Merge pull request #107 from balloob/chore-sun-entity
Sun component to use Entity ABC
2015-04-26 06:01:11 -07:00
Andrew Thigpen
3cff05ef91 Create entity for each sensor variable.
Refactors to create a separate entity for each variable associated with
a child sensor.
2015-04-25 21:08:49 -05:00
Paulus Schoutsen
bc3af134f9 Minor cleanup demo component 2015-04-25 17:44:05 -07:00
Paulus Schoutsen
3650a2fa85 Sun component now uses Entity ABC 2015-04-25 17:43:22 -07:00
Ryan Kraus
c2db17df9a Merge pull request #101 from automicus/master
State decorating, ISY component update
2015-04-25 18:37:59 -04:00
Ryan Kraus
45f2f07b6d Used better method for overwritting attributes in entity. 2015-04-25 18:29:37 -04:00
Ryan Kraus
be3be0478b Fixed bug in entity helper that ignored suggestions for hiding states. 2015-04-25 14:59:27 -04:00
Ryan Kraus
97bb5bcb7d Merge remote-tracking branch 'upstream/dev' 2015-04-25 14:49:02 -04:00
Ryan Kraus
8255164eda Rearranged visibility control and image control in the configuration file. Now a single generic clause can be used to customize any attribute. 2015-04-25 14:47:15 -04:00
Paulus Schoutsen
63c5ebf428 Fix #102 - Installing PyISY no longer required to load any light 2015-04-25 06:54:40 -07:00
Paulus Schoutsen
a9006f540f Fix #102 - Installing PyISY no longer required to load any light 2015-04-25 06:07:07 -07:00
Ryan Kraus
04a98f99b7 Updated entity tests to work with new attribute overwritting logic. 2015-04-25 01:43:03 -04:00
Ryan Kraus
a95aad324f Updated a comment in the entity class. 2015-04-25 01:29:42 -04:00
Ryan Kraus
f130ad6c27 Subscribed isy994 component to EVENT_HOMEASSISTANT_STOP event to clean itself up before quitting. 2015-04-25 01:10:41 -04:00
Ryan Kraus
f77b3dbd0a Added decorate option to configuration file to allow a user to set custom images for different entities. 2015-04-25 00:39:35 -04:00
Paulus Schoutsen
31a22d4c6a Merge pull request #100 from balloob/dev
Update master with latest changes
2015-04-24 21:02:14 -07:00
Paulus Schoutsen
4d0265cb7d Add timeout to auto detect config 2015-04-24 20:57:25 -07:00
Fabian Affolter
07f62fda32 inital nma notify component 2015-04-24 22:27:16 +02:00
Paulus Schoutsen
24bb89df23 Compiled new version of frontend 2015-04-24 08:52:37 -07:00
Paulus Schoutsen
424f05a4da Add logbook to default configuration 2015-04-24 08:47:11 -07:00
Paulus Schoutsen
41b02928ef Merge pull request #99 from fabaff/typos
Minor fix
2015-04-24 06:03:39 -07:00
Fabian Affolter
46819acaff Fix two typos 2015-04-24 08:26:41 +02:00
Paulus Schoutsen
44cd5ca15e Merge pull request #98 from balloob/chore-tweak-visibility-config
Tweak visibility config
2015-04-23 10:21:11 -07:00
Paulus Schoutsen
25a900c759 Merge pull request #97 from balloob/chore-group-entity
Have group inherit from entity
2015-04-23 09:59:01 -07:00
Paulus Schoutsen
968de0557a Update CONTRIBUTING.md 2015-04-23 09:58:43 -07:00
Paulus Schoutsen
8f36cf3c81 Clean up Entity helper test code 2015-04-23 07:33:59 -07:00
Paulus Schoutsen
b855f422ef Tweak visibility config 2015-04-23 06:41:41 -07:00
Paulus Schoutsen
bbdb0320f1 Have group inherit from entity 2015-04-22 22:31:16 -07:00
Paulus Schoutsen
1dd26fcd73 Merge pull request #96 from automicus/master
Added support for ISY994 Insteon and X10 Controller (and other updates)
2015-04-22 21:37:32 -07:00
Ryan Kraus
dc4ff25d5b 1) Upped the requirement for PyISY to version 1.0.2. 2) Omitted isy994 components from coveralls tests because it requires an external controller. 2015-04-22 23:10:51 -04:00
Ryan Kraus
bd3b93f290 1) Added visibility documentation to the CONTRIBUTING.md documentation. 2) Pylint fixes to homeassistant/helpers/entity.py 2015-04-22 22:19:36 -04:00
Ryan Kraus
d779662bdd Updated get_entities.py script to use argparse module. 2015-04-22 22:02:54 -04:00
Ryan Kraus
2b6edd153b Fixed copy pasta error. 2015-04-22 21:27:49 -04:00
Ryan Kraus
ff3dacedc0 Moved card visibility logic out of the Entity class and into a VisibilityABC. Then made the Group class inherit the VisibilityABC. No duplication of code now. This is definitely better. 2015-04-22 21:21:50 -04:00
Ryan Kraus
8fcf814eb6 Changed visbility property in configuration.yaml to a hash instead of a list of a single hash. 2015-04-22 21:07:23 -04:00
Ryan Kraus
2b4c75543a 1) Merged with mainline dev branch. 2) Removed assumption in homeassistant/__init__.py that states are visible if not specified. This assumption is intrinsic in the JavaScript. 3) Recompiled frontend to assist merge. 2015-04-22 21:04:52 -04:00
Ryan Kraus
d566a328a3 pylint fix to isy switches. 2015-04-22 00:22:48 -04:00
Ryan Kraus
99ea0dc59d Updated requirements to include PyISY. 2015-04-21 23:59:56 -04:00
Ryan Kraus
4d91c4a51b Updated contributing documentation to include details about hidding states. 2015-04-21 23:58:41 -04:00
Ryan Kraus
3b0c685679 Merge remote-tracking branch 'upstream/master' 2015-04-21 23:56:50 -04:00
Paulus Schoutsen
0e2cf6532b Merge pull request #95 from kixam/master
simplistic Modbus implementation
2015-04-21 15:00:06 -07:00
Aurélien Correia
4d6555441d Passed Travis CI 2015-04-21 16:40:13 +02:00
Aurélien Correia
5ce95f6b88 Merge remote-tracking branch 'upstream/master' 2015-04-21 15:26:26 +02:00
Paulus Schoutsen
23902a62d7 Merge pull request #94 from wind-rider/patch-1
Update CONTRIBUTING.md
2015-04-20 21:46:58 -07:00
wind-rider
be9a1d6b27 Update CONTRIBUTING.md
Updated icon list url
2015-04-21 00:12:39 +02:00
Ryan Kraus
0032e4b6cf On second thought, make that script use the specified order, not a sorted order. 2015-04-18 00:30:09 -04:00
Ryan Kraus
9a2e6dcba5 Added a script for listing entities in running Home Assistant server. Usefule for creating visibility list in configuration file. 2015-04-18 00:26:40 -04:00
Ryan Kraus
da4cf61a09 Forced the isy994 component to treat underscores as spaces. 2015-04-17 09:30:20 -04:00
Ryan Kraus
6b2dd69bcb Updated isy994 component to hide any device with the HIDDEN STRING in its ancestry. 2015-04-17 09:27:14 -04:00
Aurélien Correia
fb6b514c34 Adding simplistic support for Modbus sensor and switch (based on pymodbus) 2015-04-15 16:47:42 +02:00
Ryan Kraus
b20424261c 1) Performed many pylint and flake8 fixes to clean up isy994 integration and hidden entities addition. 2) Added necessary code to allow groups to also be hidden. 3) Made most of the weather data from the isy994 component be hidden by default. 2015-04-15 02:05:34 -04:00
Ryan Kraus
caed69d5ea Added state card hiding to the STATE view on the frontend. 2015-04-14 23:55:08 -04:00
Ryan Kraus
0334074a52 Quick fix to the comparison to validate if an entity is hidden. 2015-04-14 23:38:14 -04:00
Ryan Kraus
a3d6972268 1) Added basic back-end framework for supporting hidden entities. 2) Enabled hidden suggestions in the isy994 component entities. 2015-04-14 22:57:32 -04:00
Ryan Kraus
c76644323f Updated the broken link to the apple-touch icon in the frontend. 2015-04-14 01:44:39 -04:00
Ryan Kraus
83aea10f06 Added hidden_string and sensor_string properties to the isy994 configuration to allow nodes to be hidden and to be handled as sensors. Implimented the sensor_string. Any node name that contains the sensor_string in its name will be treated as a sensor instead of a switch or light. The hidden_string will be implimented later. 2015-04-13 12:56:37 -04:00
Ryan Kraus
0e9a8a7cc2 Added custom program sensors to the isy994 component. 2015-04-13 01:47:32 -04:00
Ryan Kraus
510064d9c8 Added support for the creation of custom switches using the ISY994 device. 2015-04-12 22:30:14 -04:00
Ryan Kraus
4e3ccfffbb Added not dimming Insteon devices on an ISY controller as switches. 2015-04-12 17:18:14 -04:00
Ryan Kraus
f6d75f2db2 Cleaned up ISY994 light and sensor code to use the same abstract class. 2015-04-12 16:45:23 -04:00
andythigpen
dfbeddf1f7 Update .coveragerc 2015-04-11 11:29:23 -05:00
James Cole
2dd96c584b Merge pull request #91 from jamespcole/line-chart-dev
Removed interactivity on the small more-info version of the line graphs that was causing scrollbars issues
2015-04-11 19:09:46 +10:00
jamespcole
c75e145151 rebuild of the frontend 2015-04-11 19:02:44 +10:00
jamespcole
a478ec6697 Removed interactivity from small view of line charts which was causing scrollbar issues on more-info card 2015-04-11 19:01:17 +10:00
James Cole
992a926592 Merge pull request #88 from jamespcole/transmission-dev
Adds basic support for Transmission torrent client as a sensor.
2015-04-08 00:57:05 +10:00
jamespcole
439b7be77d Added the new transmission sensor to the coveragerc file 2015-04-08 00:35:57 +10:00
James Cole
5eabc15a7d Merge pull request #87 from jamespcole/dev
Added init script for HA on the raspberry pi
2015-04-08 00:14:57 +10:00
jamespcole
4f222e64e6 Added init script for HA on the raspberry pi 2015-04-08 00:07:24 +10:00
jamespcole
df9e8f5214 Merge branch 'dev' into transmission-dev
Merging in the latest from the dev branch
2015-04-07 23:47:57 +10:00
James Cole
20e07a8387 Merge pull request #86 from jamespcole/line-chart-dev
Merged new line chart history view for devices with a unit_of_measurement specified
2015-04-07 23:45:50 +10:00
jamespcole
e63d0d7aac Tidied up the documentations and linting warnings 2015-04-07 23:21:41 +10:00
jamespcole
e65ad67b32 Fixed some alignment issues and added loading spinner 2015-04-07 22:21:13 +10:00
jamespcole
56184daf59 Fixed linting warnings 2015-04-07 18:01:23 +10:00
jamespcole
100a75908b Fixed bug in history component where the entity id filtering was not being applied correctly 2015-04-07 02:25:03 +10:00
jamespcole
ba13f78d49 Added initial Transmission torrent client sensor 2015-04-06 22:13:04 +10:00
jamespcole
89527d3bb2 rebuilt frontend with line graph support 2015-04-06 18:26:47 +10:00
jamespcole
8151de1ae0 Added zoom options to line graphs 2015-04-06 18:15:13 +10:00
jamespcole
7d997f7699 updated the ha js 2015-04-06 15:40:26 +10:00
jamespcole
cae2a11f7b removed unneeded loading messages 2015-04-06 15:22:52 +10:00
jamespcole
b1f01506ce Added in some comments 2015-04-06 14:37:01 +10:00
Andrew Thigpen
4d47d313f9 Add mysensors support for metric/imperial units. 2015-04-05 17:25:10 -05:00
Andrew Thigpen
0e9d826d41 Push mysensor state instead of polling. 2015-04-05 17:25:10 -05:00
Andrew Thigpen
c72a735851 Use stop method for mysensors on shutdown. 2015-04-05 17:25:09 -05:00
Andrew Thigpen
c41d7b8f6d Refactor and update to use latest pymysensors. 2015-04-05 17:25:06 -05:00
jamespcole
264bc15091 Fixed the line charts 2015-04-05 02:11:27 +10:00
jamespcole
b6e880ed73 Fixed the line charts 2015-04-05 02:03:16 +10:00
jamespcole
12d88c2c2a Added loading spinner to the history screen 2015-04-05 01:06:04 +11:00
jamespcole
c14fa299e1 Improved the efficiency and accuracy of the line graphs 2015-04-05 00:49:52 +11:00
Ryan Kraus
57f27cc97a Addded light controls to isy994 component. 2015-04-04 06:13:27 -04:00
Ryan Kraus
80f1581d6e Added ISY994 weather data as sensors. 2015-04-04 04:33:03 -04:00
jamespcole
a86c53236d refactored line chart to be more efficient 2015-04-04 17:55:30 +11:00
jamespcole
2576659923 refactored line graph code 2015-04-04 17:27:05 +11:00
jamespcole
bc54d9777d refactored line graph code 2015-04-04 17:21:15 +11:00
jamespcole
b50023ede1 refactored line graph code 2015-04-04 16:28:13 +11:00
jamespcole
9499a8297a Merge branch 'dev' into line-chart-dev
merging in the latest changes
2015-04-04 11:07:28 +11:00
jamespcole
24dbc1f3d9 merged with upstream 2015-04-04 08:55:40 +11:00
Paulus Schoutsen
5ec686b030 Merge pull request #81 from balloob/dev
Update master with latest changes
2015-04-03 00:32:13 -07:00
theolind
e87652e95f added HA module for mysensors 2015-04-02 19:59:44 +02:00
Paulus Schoutsen
b0bf775da8 Compile new version frontend 2015-04-01 21:49:03 -07:00
Paulus Schoutsen
e43eee2eb1 Style fixes 2015-04-01 07:18:03 -07:00
Paulus Schoutsen
57b3e8018b Logbook bug fixes 2015-03-31 23:09:08 -07:00
Paulus Schoutsen
00bbc17e11 Add State.last_updated to JSON obj 2015-03-31 23:08:38 -07:00
Paulus Schoutsen
b02c11c31d Merge pull request #80 from balloob/component-logbook
Add component logbook
2015-03-31 09:12:03 -07:00
Paulus Schoutsen
30e7f09000 Clean up logbook component 2015-03-30 00:19:56 -07:00
Paulus Schoutsen
742479f8bd Have logbook group HA stop + start 2015-03-30 00:11:24 -07:00
Paulus Schoutsen
6455f1388a Have logbook only report each sensor every 15 minutes 2015-03-29 23:57:52 -07:00
Paulus Schoutsen
9fb634ed3a Fix type in CSS class name 2015-03-29 15:23:50 -07:00
Paulus Schoutsen
100081757c Add logbook to frontend 2015-03-29 14:50:41 -07:00
Paulus Schoutsen
234bfe1199 Add logbook component 2015-03-29 14:47:21 -07:00
Paulus Schoutsen
a2f8fa7b05 Add time_fired to Event class 2015-03-29 14:47:20 -07:00
Paulus Schoutsen
a21673069b Bugfix for states at point in time on new databases 2015-03-29 09:42:24 -07:00
James Cole
1c593c9c3c Merge pull request #79 from jamespcole/vera-dev
Refactored vera temp sensors to use temperature contstants and conversion
2015-03-30 00:59:43 +11:00
jamespcole
c067cddbe8 Refactored vera temp sensors to use temperature contstants and conversion 2015-03-30 00:51:03 +11:00
Paulus Schoutsen
3e525fbe1b Merge pull request #77 from jamespcole/ddwrt-dev
Ddwrt dev
2015-03-28 17:52:17 -07:00
jamespcole
0b6d260fa6 fixed flake8 blank lines error 2015-03-29 11:49:07 +11:00
jamespcole
007d0d9ce9 Added ddwrt component to coveragerc 2015-03-29 11:44:02 +11:00
jamespcole
fda44cdbf7 Moved compiled regex to a constant for efficiency 2015-03-29 11:40:21 +11:00
jamespcole
242c143c85 refactored ddwrt data format parsong code 2015-03-29 11:30:04 +11:00
Paulus Schoutsen
522bbfb716 Expose to more info content if dialog is open 2015-03-28 13:41:07 -07:00
jamespcole
a959c48708 Fixed travis another CI indenting error 2015-03-28 19:17:51 +11:00
jamespcole
a9ce12be34 Fixed travis CI indenting error 2015-03-28 18:59:12 +11:00
jamespcole
05239c26f9 Forgot to add README.md file changes to last commit 2015-03-28 18:50:24 +11:00
jamespcole
fc07032d35 Fixed some code formatting and added dd-wrt to the readme 2015-03-28 18:43:41 +11:00
jamespcole
7e6af57186 FIxed some linting issues 2015-03-28 18:29:45 +11:00
Paulus Schoutsen
5a0251c3cd ps: Fix recorder.py fetching wrong run information 2015-03-27 23:11:07 -07:00
jamespcole
c8c38e498a Added a device tracker for dd-wrt routers 2015-03-28 03:51:33 +11:00
Paulus Schoutsen
70bce24b0a Merge pull request #73 from balloob/nmap-lock
Improve NMAP lock issues
2015-03-26 14:17:09 -07:00
Paulus Schoutsen
4484baa866 Remove lock and add host timeout to NMAP scanner 2015-03-25 22:50:51 -07:00
Paulus Schoutsen
c3fc19353b Fix device tracker waiting forever when platform gets stuck 2015-03-25 22:50:20 -07:00
Paulus Schoutsen
cd60f9a8e2 Merge pull request #71 from Jaidan/ignores
Add python-version file to gitignore
2015-03-23 13:22:01 -07:00
John Williams
8d78651606 Add python-version file to gitignore 2015-03-23 20:18:07 +00:00
Paulus Schoutsen
ea200dea40 Merge pull request #70 from balloob/dev
Update master with latest changes
2015-03-21 23:26:00 -07:00
Paulus Schoutsen
ec557f8d44 Fix broken tellstick and systemmonitor sensors 2015-03-21 22:26:41 -07:00
Paulus Schoutsen
49d7901585 Tweaks to EntityComponent.setup_platform 2015-03-21 22:21:57 -07:00
Paulus Schoutsen
cdd5d1196a Fix regression from refactoring EntityComponent 2015-03-21 22:14:30 -07:00
Paulus Schoutsen
06d85595a0 Update coveragerc 2015-03-21 22:09:25 -07:00
Paulus Schoutsen
2863c2d593 Made bootstrap.setup_component more robust 2015-03-21 22:06:59 -07:00
Paulus Schoutsen
58812b326c Move hass.local_api and hass.components to config object 2015-03-21 22:06:59 -07:00
James Cole
609064b9d8 Merge pull request #68 from jamespcole/nzb-dev
Fixed attribute error with new unit_of_measurement changes
2015-03-22 16:06:34 +11:00
Paulus Schoutsen
64435c87df Merge pull request #69 from jamespcole/pushover-dev
Pushover notification support
2015-03-21 22:02:56 -07:00
jamespcole
470096047b removed unused imports 2015-03-22 15:43:59 +11:00
jamespcole
7a2aa43caa Fixed incorrect URL in config instructions 2015-03-22 15:37:43 +11:00
jamespcole
58fd07e4c6 fixed the flake8 and pylint warnings 2015-03-22 15:32:47 +11:00
jamespcole
04d16d7607 removed unused state attributes that are no longer required after upstream changes 2015-03-22 15:18:58 +11:00
jamespcole
20f52a7fb1 Made API Key a required config variable 2015-03-22 15:13:57 +11:00
jamespcole
e877ed01af Fixed attribute error with new unit_of_measurement changes 2015-03-22 14:43:20 +11:00
jamespcole
a0f1c1d17a Added support for pushover notifications 2015-03-22 14:36:58 +11:00
Paulus Schoutsen
e872435870 Tellstick sensor: Lookup sensor ids as ints 2015-03-21 19:38:43 -07:00
Paulus Schoutsen
bbfd97e2b8 Migrate components to use EntityComponent 2015-03-21 19:37:18 -07:00
Paulus Schoutsen
a9324ba9d4 Update components to use Entity instead of Device 2015-03-21 19:16:13 -07:00
Paulus Schoutsen
d3f0210b1a Refactor helper.device to helper.entity
Introduces a minor backwards compatible change: device_component
function add_devices is renamed to add_entities.
2015-03-21 18:49:30 -07:00
jamespcole
60fbc51a2d added in line graph support for state history 2015-03-22 06:10:24 +11:00
Paulus Schoutsen
c8401a3c4d Merge pull request #67 from balloob/ha-config
Add a global config object to Home Assistant
2015-03-21 00:53:30 -07:00
Paulus Schoutsen
b633f83bd9 Merge pull request #53 from jamespcole/nzb-dev
Added in SABnzbd sensor
2015-03-20 18:17:27 -07:00
jamespcole
b477514e58 Added in API availability check and better exception handling 2015-03-21 12:10:38 +11:00
jamespcole
30c78b4054 Fixed gitmodules merge conflict 2015-03-21 10:14:03 +11:00
Paulus Schoutsen
1f453c9394 Add default datatype mask to tellstick sensor 2015-03-19 12:34:22 -07:00
Paulus Schoutsen
7a7f486cb2 Rename config.get_config_path to config.path 2015-03-19 12:27:56 -07:00
Paulus Schoutsen
9b643d57f0 ps: Add a global config object to Home Assistant 2015-03-18 23:02:58 -07:00
Paulus Schoutsen
569b15d790 Some Z-Wave fixes 2015-03-18 19:15:48 -07:00
Paulus Schoutsen
8ff1bcf242 Fix some Firefox style issues 2015-03-16 23:52:50 -07:00
Paulus Schoutsen
1245af356b Scene bugfixes and UI improvements 2015-03-16 23:32:18 -07:00
Paulus Schoutsen
b459a29947 Fix style issues 2015-03-16 22:45:42 -07:00
Paulus Schoutsen
83d83a09b5 Add scene to demo 2015-03-16 22:35:57 -07:00
Paulus Schoutsen
71803658f5 Updates to demo component and platforms 2015-03-16 22:20:35 -07:00
Paulus Schoutsen
a4af51af14 No longer auto load recorder and group components 2015-03-16 22:20:35 -07:00
Paulus Schoutsen
a2558972b9 Empty configuration.yaml no longer crashes setup 2015-03-16 22:20:35 -07:00
Paulus Schoutsen
b0a09f01cc Minor frontend bugfixes 2015-03-16 22:20:35 -07:00
Paulus Schoutsen
18f27d2e45 Merge pull request #66 from balloob/scene-component
Scene component
2015-03-16 22:19:56 -07:00
Paulus Schoutsen
19a43cea26 Add scene component 2015-03-15 23:36:42 -07:00
Paulus Schoutsen
36feb7f50a Remove unneeded dependencies from Dockerfile 2015-03-15 16:00:03 -07:00
Paulus Schoutsen
5db6f81f9c Merge pull request #64 from trainman419/correct_lights_off
Turn off correct lights
2015-03-15 15:56:16 -07:00
trainman419
730faed757 Turn off correct lights
Only turn off the lights specified in the sun light trigger.
I use this to turn off my bedroom lights when I leave, even if my
housemates are still home.
2015-03-15 15:50:32 -07:00
Paulus Schoutsen
06d4f5b0db Merge pull request #63 from trainman419/mac_upper_dev
Convert MAC addresses to uppercase on load
2015-03-15 14:46:01 -07:00
trainman419
e891162dad Convert MAC addresses to uppercase on load
This fixed an issue for me where my known_devices file had lowercase MAC
addresses, but the device tracker returns uppercase MAC addresses.
2015-03-15 14:43:45 -07:00
Paulus Schoutsen
0b8fea923a Update build python openzwave 2015-03-15 00:05:18 -07:00
Paulus Schoutsen
ef5deac3e1 Fix Z-Wave sensors not showing up 2015-03-15 00:01:18 -07:00
Paulus Schoutsen
1d33d7d124 Z-Wave sensors no longer auto associate with controller 2015-03-14 20:20:48 -07:00
Paulus Schoutsen
a1c6d9d1da Merge pull request #60 from andythigpen/feature/scripts
Script component
2015-03-14 19:38:13 -07:00
andythigpen
1b73e15ec7 Update the frontend. 2015-03-14 21:29:55 -05:00
andythigpen
0b751949ae Fix JS notification error on initialization.
The last notification is initially null resulting in an exception being
thrown on startup.
2015-03-14 21:29:55 -05:00
andythigpen
d0f545c247 Fix always showing scroll bars on side menu. 2015-03-14 21:29:55 -05:00
andythigpen
046efe3acb Adds script component.
A script is composed of a sequence of actions (currently service calls)
that are executed in order.  Individual actions can also be delayed by a
given timedelta.
2015-03-14 21:29:55 -05:00
Paulus Schoutsen
5f734f4dd9 Merge branch 'dev'
* dev:
  Fix a config bug in Automation
2015-03-14 19:20:53 -07:00
Paulus Schoutsen
948a5c97ec Fix a config bug in Automation 2015-03-14 19:18:41 -07:00
Paulus Schoutsen
1dc002f180 Merge branch 'dev'
* dev:
  Fix a config bug in Automation
  Fixes for remote instances Home Assistant
  Fixes for new release PyLint
2015-03-14 19:13:15 -07:00
Paulus Schoutsen
6da0257bb6 Fix a config bug in Automation 2015-03-14 19:13:03 -07:00
Paulus Schoutsen
633d0453be Fixes for remote instances Home Assistant 2015-03-14 13:07:38 -07:00
Paulus Schoutsen
aec861c1a4 Fixes for new release PyLint 2015-03-14 12:39:17 -07:00
Paulus Schoutsen
cdeceb140d Fixes for new release PyLint 2015-03-14 12:38:30 -07:00
Paulus Schoutsen
ebd597b811 Merge pull request #59 from balloob/dev
Update master with latest changes
2015-03-14 12:31:28 -07:00
Paulus Schoutsen
284500ff21 Upgrade to latest home assistant js 2015-03-14 00:00:51 -07:00
Paulus Schoutsen
6242b856b4 Some bug fixes related to new Home Assistant JS 2015-03-11 00:21:18 -07:00
Paulus Schoutsen
020213e682 Upgrade to latest Home-Assistant-JS 2015-03-10 22:40:55 -07:00
Paulus Schoutsen
c7ae0f5f90 Reposition speech-to-text progress 2015-03-10 22:40:50 -07:00
Paulus Schoutsen
5b9b744662 Merge pull request #58 from andythigpen/config-lists
Add support for lists when using config_per_platform helper.
2015-03-10 17:40:49 -07:00
andythigpen
06ad3987ba Add support for lists when using config_per_platform helper.
With the recent change to YAML, it is now easier to support lists by
default.  Any config section that previous relied on the "domain",
"domain 1", "domain 2" format can now use YAML lists instead.  The old
format is also still supported.
2015-03-10 18:30:36 -05:00
Paulus Schoutsen
c46e27928f Merge pull request #57 from balloob/component-conversation
Add conversation component
2015-03-10 08:52:10 -07:00
Paulus Schoutsen
eaded9b67c Add support for voice commands 2015-03-10 00:20:52 -07:00
Paulus Schoutsen
90eb8aa82a Move icon imports to generic file 2015-03-10 00:09:08 -07:00
Paulus Schoutsen
e915dd0020 Add a basic conversation component 2015-03-10 00:08:50 -07:00
Paulus Schoutsen
e32c2d2569 Deprecate process component 2015-03-08 23:52:56 -07:00
Paulus Schoutsen
74d243ac41 Merge pull request #56 from jamespcole/vera-dev
Fixed variable scoping
2015-03-08 21:24:58 -07:00
jamespcole
06c3087310 Fixed variable scoping 2015-03-09 15:16:02 +11:00
Paulus Schoutsen
aa0c6ef7c2 Make remember option in login form not bold 2015-03-08 20:39:10 -07:00
Paulus Schoutsen
a7e4d5102e Merge pull request #52 from jamespcole/vera-dev
Vera Z-Wave Controller Support
2015-03-08 16:03:23 -07:00
jamespcole
56622596e7 Changed exception logging, and updated excluded device logic 2015-03-09 09:11:59 +11:00
jamespcole
7ee37648d8 Updated exception type for errors when connecting to vera 2015-03-09 08:45:20 +11:00
jamespcole
95852db18d Changed the configuration to use hashes instead of searching array 2015-03-09 08:34:06 +11:00
Paulus Schoutsen
532e9f5de2 Merge pull request #54 from andythigpen/fix-blank-password
Issue with blank password
2015-03-08 14:31:45 -07:00
andythigpen
399b433a06 Fix issue causing blank passwords to no longer be accepted. 2015-03-08 16:21:35 -05:00
jamespcole
7dc3198320 Added constants for armed, tripped and tripped time 2015-03-09 08:10:31 +11:00
Paulus Schoutsen
af6407c1df Merge pull request #50 from andythigpen/feature/variable-nmap-tracker
Variable nmap tracker
2015-03-08 14:01:31 -07:00
andythigpen
bfb5089ed5 Add configurable interval for nmap_tracker when devices are home.
Adds an option that will exclude devices from the periodic nmap scan
for the configured number of minutes.
2015-03-08 15:54:00 -05:00
jamespcole
38fbc3595a Added spcific exception type for failure to communicate with Vera controller 2015-03-09 07:46:26 +11:00
Paulus Schoutsen
c85e9625a7 Merge pull request #49 from andythigpen/dev
Fix issue with automation event data after YAML conversion.
2015-03-08 13:32:22 -07:00
jamespcole
6dc18ba603 removed the VeraLight class and just switched it to using VeraSwitch instead 2015-03-09 07:24:34 +11:00
jamespcole
7772d5af62 moved setting name to constructor 2015-03-09 07:15:41 +11:00
jamespcole
a2f438c6ef now using ATTR_BATTERY_LEVEL 2015-03-09 07:11:35 +11:00
jamespcole
1b29d61562 Made exception handling more specific 2015-03-09 07:03:56 +11:00
jamespcole
efdb54cbe4 Added in SABnzbd sensor 2015-03-09 05:28:12 +11:00
jamespcole
7a21e8a3fb Fixed flake8 comment warning 2015-03-09 02:08:46 +11:00
jamespcole
50ff26ea20 Fixed flake8 errors 2015-03-09 01:58:11 +11:00
jamespcole
f38787c258 reverted gitignore changes 2015-03-09 01:20:56 +11:00
jamespcole
42dc973ccc Fixed up linting errors 2015-03-09 01:14:44 +11:00
jamespcole
dc8147c46d Changed vera components over to new config file format 2015-03-08 23:52:50 +11:00
jamespcole
a30d1dcfef Merge branch 'dev' of https://github.com/balloob/home-assistant into dev
Getting the latest from balloob
2015-03-08 22:08:03 +11:00
Theodor Lindquist
851bff1e12 Merge pull request #51 from balloob/dev
Merge dev into master
2015-03-08 08:56:01 +01:00
Theodor Lindquist
e6ed2e58b0 Merge pull request #48 from theolind/dev
Added process watching functionality to sensor.systemmonitor
2015-03-08 08:45:17 +01:00
theolind
0f1307cd81 Fixed http being too strict on the api_password config 2015-03-08 08:41:28 +01:00
andythigpen
bb858fdddc Fix issue with automation event data after YAML conversion.
No need to convert to dict using json.loads when PyYAML will do that
automatically.
2015-03-07 16:10:35 -06:00
theolind
43f0014200 Added process watching functionality to sensor.systemmonitor 2015-03-07 20:54:52 +01:00
Paulus Schoutsen
c3c07d621a Tweak coveragerc a bit more 2015-03-07 09:00:31 -08:00
Paulus Schoutsen
7e3de4d6f7 Don't include device specific code in coverage 2015-03-07 07:49:19 -08:00
Theodor Lindquist
7880b6a11d Merge pull request #47 from theolind/dev
Added a system monitoring platform to the sensor component
2015-03-07 15:58:04 +01:00
theolind
1215b5e994 Fixed type issue 2015-03-07 15:55:32 +01:00
theolind
828c78cb1f fixed style errors 2015-03-07 15:49:58 +01:00
theolind
b42b680ef5 Merge remote-tracking branch 'upstream/dev' into dev
Conflicts:
	requirements.txt
2015-03-07 15:34:47 +01:00
theolind
d7a4242c74 Specified the component that uses psutil (sensor.systemmonitor) 2015-03-07 15:31:09 +01:00
theolind
dd92173576 Made example config more YAMLy 2015-03-07 15:27:31 +01:00
theolind
3173249dc3 fixed typo 2015-03-07 15:24:35 +01:00
theolind
97258747c7 Renamed device to resource 2015-03-07 15:22:03 +01:00
theolind
19964e914a Added a system monitoring platform to the sensor component 2015-03-07 14:54:08 +01:00
Paulus Schoutsen
d3e107a882 Clean up requirements.txt 2015-03-07 05:49:22 -08:00
jamespcole
250f35c2a5 Added documentation for proposed format of YAML config 2015-03-07 14:49:20 +11:00
jamespcole
fceac45ddd Updated vera switch and sensor to the new architecture 2015-03-07 12:59:13 +11:00
jamespcole
ba58c080e3 removed old config example to fix merge conflict 2015-03-07 12:36:08 +11:00
jamespcole
ff0099b6a7 Merge branch 'master' of https://github.com/balloob/home-assistant
Merging with the upstream master to get the compnent architecture changes
2015-03-07 12:25:49 +11:00
jamespcole
5536f1621d fixing issue with built files and upstream merge 2015-03-07 12:25:41 +11:00
Paulus Schoutsen
fde0ce1997 Remove CONF_TYPE and platform_devices_from_config 2015-03-06 00:04:32 -08:00
Paulus Schoutsen
3e15742875 Move device ABCs to separate helper file 2015-03-05 23:18:22 -08:00
Paulus Schoutsen
68668fc658 Light/Switch/Thermostat: use new extract_from_service helper 2015-03-05 23:17:05 -08:00
Paulus Schoutsen
72b930af8f Fix: Hue reports state correctly if 2 services called quickly 2015-03-05 22:53:11 -08:00
Paulus Schoutsen
1b4ff986b0 Add remember login option 2015-03-05 21:37:44 -08:00
Paulus Schoutsen
4e6773969a Fix: Login form shows error message again 2015-03-05 20:43:40 -08:00
Paulus Schoutsen
ba9f29a04b Fix http/zwave being too strict on the config 2015-03-05 20:43:20 -08:00
Paulus Schoutsen
b04d6f94e6 Merge pull request #45 from balloob/tellstick-sensor-refactor
Refactor Tellstick sensors to be a sensor platform
2015-03-05 08:44:00 -08:00
Paulus Schoutsen
a09c5d88db Update favicon.ico 2015-03-05 00:19:13 -08:00
Paulus Schoutsen
be9d6b9422 Remove forced orientation from manifest.json 2015-03-04 09:23:14 -08:00
Paulus Schoutsen
84844c242b Refactor chromecast into media_player platform 2015-03-03 23:50:54 -08:00
Paulus Schoutsen
fc3375508e Frontend: Remove tellstick_sensor domain icon 2015-03-03 22:20:48 -08:00
Paulus Schoutsen
663735542b Refactor tellstick_sensor to a sensor platform 2015-03-03 22:19:29 -08:00
Paulus Schoutsen
a90dcabe01 Higher quality favicon.ico 2015-03-03 21:49:20 -08:00
Paulus Schoutsen
fa9f5073f6 Update logo 2015-03-03 21:15:21 -08:00
Paulus Schoutsen
9616a2292e Add manifest.json 2015-03-03 21:15:15 -08:00
Paulus Schoutsen
b4f743bda3 ZWave component now reports init success 2015-03-03 08:49:31 -08:00
jamespcole
138eadedcc undid unnecessary changes to gitignore 2015-03-02 23:08:05 +11:00
jamespcole
53de2ad86d Added __pycache__ to gitignore 2015-03-02 22:28:40 +11:00
jamespcole
8dfc91a502 Added vera components and added __pycache__ to gitignore 2015-03-02 22:05:03 +11:00
jamespcole
3449b3d1d7 vera device types 2015-03-02 21:09:00 +11:00
jamespcole
7b62cf4af8 Added in vera api submodule 2015-03-02 21:02:59 +11:00
Paulus Schoutsen
2727fd12ee Merge pull request #42 from balloob/chore-refactor-device-components
Refactor device components
2015-03-01 11:12:03 -08:00
Paulus Schoutsen
00047009e2 Only poll for device updates if necessary 2015-03-01 11:04:07 -08:00
Paulus Schoutsen
0fd89a4b1f Fix a WeMo discovery issue 2015-03-01 10:43:08 -08:00
Paulus Schoutsen
7938cbffd4 Suppress specific import errors in the loader 2015-03-01 10:40:07 -08:00
Paulus Schoutsen
5fe50066a6 Make device component backwards compatible 2015-03-01 10:28:53 -08:00
Paulus Schoutsen
3b7b34b3df Fix Style 2015-03-01 01:39:28 -08:00
Paulus Schoutsen
508a433a92 Fix Style 2015-03-01 01:39:12 -08:00
Paulus Schoutsen
fce3e239a7 Update import from netdisco 2015-03-01 01:36:19 -08:00
Paulus Schoutsen
89100d14c8 Refactored device components 2015-03-01 01:35:58 -08:00
Paulus Schoutsen
f0c6ac1aa3 Update import from netdisco 2015-03-01 01:34:49 -08:00
Paulus Schoutsen
63bf1373b7 Disable pylint import error check 2015-02-28 23:02:26 -08:00
Paulus Schoutsen
19d243d159 ZWave Sensor values push changes to HA 2015-02-28 22:49:55 -08:00
Paulus Schoutsen
67161d686b Use PyDispatcher instead of Louie 2015-02-28 22:49:27 -08:00
Paulus Schoutsen
8bd803601f Devices can now be polling or push 2015-02-28 22:33:44 -08:00
Paulus Schoutsen
8567dd001b Merge remote-tracking branch 'origin/feature-openzwave' into feature-openzwave
* origin/feature-openzwave:
  Update zwave related scripts
  Z-Wave sensors should work now
  Register to Z-Wave sensor updates
  Get Z-Wave sensors to work with Home Assistant
  Minor documentation updates
  Working zwave!
  Get python-openzwave working in Docker

Conflicts:
	scripts/build_python_openzwave
2015-02-28 21:08:24 -08:00
Paulus Schoutsen
7636769c11 Update discovery to work without zeroconf installed 2015-02-28 21:06:59 -08:00
Paulus Schoutsen
7c2d92c55d Update zwave related scripts 2015-02-28 20:27:42 -08:00
Paulus Schoutsen
fa76cb8f8c Z-Wave sensors should work now 2015-02-28 20:27:42 -08:00
Paulus Schoutsen
1b00515899 Register to Z-Wave sensor updates 2015-02-28 20:27:42 -08:00
Paulus Schoutsen
e7b9b86c64 Get Z-Wave sensors to work with Home Assistant 2015-02-28 20:27:42 -08:00
Paulus Schoutsen
85f7f5589d Minor documentation updates 2015-02-28 20:27:42 -08:00
Paulus Schoutsen
9c61c281ca Working zwave!
Open docker, go to /usr/src/balloob-python-openzwave/examples, run
python3 api_demo.py --device=/zwaveusbstick --log=Debug
2015-02-28 20:27:42 -08:00
Paulus Schoutsen
bb5e8e00dd Get python-openzwave working in Docker 2015-02-28 20:27:41 -08:00
Paulus Schoutsen
b38146bdef Auto set friendly name in state attributes 2015-02-28 20:10:39 -08:00
Paulus Schoutsen
80ffe74af6 Group component now supports lists in the config 2015-02-28 19:22:16 -08:00
Paulus Schoutsen
004d4ed123 Ensure the sun latitude and longitude are strings 2015-02-28 19:14:32 -08:00
Paulus Schoutsen
28059bda7f Configuration example tweaks 2015-02-28 19:14:00 -08:00
Paulus Schoutsen
3ebfc0e917 Merge pull request #41 from theolind/feature_yaml_config
Support for YAML configuration
2015-02-28 15:03:38 -08:00
theolind
1206c2113c Fixed style error, a line was too long 2015-02-28 20:31:26 +01:00
theolind
461e0d0314 Now converting config values to dictionaries if they are None 2015-02-28 20:17:50 +01:00
theolind
8da1fb1d74 Added example yaml config file, removed old example config, fixed style violation in bootstrap.py 2015-02-28 18:59:45 +01:00
Paulus Schoutsen
98f3e6ed64 Update zwave related scripts 2015-02-28 09:09:32 -08:00
Paulus Schoutsen
bbe19cbbb0 Add more info card for thermostat. Fixes #28 2015-02-28 08:48:33 -08:00
theolind
f2b602c7ec Added logic to main that prefers .yaml-config over .conf 2015-02-28 17:42:23 +01:00
theolind
223d2c2c3f Added support for YAML files, now with included python modules 2015-02-28 17:05:38 +01:00
theolind
356732189c Added support for YAML configuration files 2015-02-28 16:56:58 +01:00
Paulus Schoutsen
b2b82d955c Break demo component into small demo platforms 2015-02-28 07:31:39 -08:00
Paulus Schoutsen
f356723e20 Upgraded to Polymer 0.5.5 2015-02-27 18:53:22 -08:00
Paulus Schoutsen
4ef17270b5 Merge branch 'dev'
* dev:
  Fix device tracker tests
2015-02-27 18:45:13 -08:00
Paulus Schoutsen
b73fd6d522 Fix device tracker tests 2015-02-27 18:45:06 -08:00
Paulus Schoutsen
6b5920b98b Merge branch 'dev'
* dev:
  Device tracker: make all Mac addresses uppercase. Fixes #37
  ps: Slim down README.md
  Update __init__.py
  Remove no longer supported options from example
  Adds event automation module.
2015-02-27 18:37:54 -08:00
Paulus Schoutsen
f6f76acdb0 Device tracker: make all Mac addresses uppercase. Fixes #37 2015-02-27 18:36:09 -08:00
Paulus Schoutsen
ee20268e27 ps: Slim down README.md 2015-02-27 18:29:49 -08:00
Paulus Schoutsen
0b9cf6384e Merge pull request #39 from jamespcole/patch-1
Update __init__.py
2015-02-27 08:59:33 -08:00
James Cole
cda821e649 Update __init__.py
Updated to fix the following error when loading sensors:

"home-assistant/homeassistant/components/sensor/__init__.py", line 87, in setup
    hass.track_time_change(update_sensor_states, seconds=range(0, 60, 3))
TypeError: track_time_change() got an unexpected keyword argument 'seconds'"

the named parameter was using seconds instead of second
2015-02-28 03:53:07 +11:00
Paulus Schoutsen
71ca07363a Z-Wave sensors should work now 2015-02-25 23:27:17 -08:00
Paulus Schoutsen
b73700d139 Remove no longer supported options from example 2015-02-24 21:39:46 -08:00
Paulus Schoutsen
88ce2255f9 Merge pull request #38 from andythigpen/feature/automation-event
Adds event automation module.
2015-02-23 19:03:20 -08:00
andythigpen
7951137693 Adds event automation module.
When events are fired with matching data, the automation.event module
executes the corresponding action for automation rules.
2015-02-23 20:42:37 -06:00
Paulus Schoutsen
015e7f8acf Merge pull request #36 from andythigpen/feature/device-tracker-interval
Add configurable intervals to device tracker.
2015-02-23 17:47:13 -08:00
andythigpen
22a2b65e3f Add configurable intervals to device tracker.
Allows the user to configure a longer interval for when the device
tracker should scan for devices.
2015-02-23 19:32:06 -06:00
Paulus Schoutsen
e9218e2eb2 Register to Z-Wave sensor updates 2015-02-23 00:01:04 -08:00
Paulus Schoutsen
a013ccf806 Get Z-Wave sensors to work with Home Assistant 2015-02-22 17:36:28 -08:00
Paulus Schoutsen
7f7a1f2740 Minor documentation updates 2015-02-22 17:33:01 -08:00
Paulus Schoutsen
3239c04368 Working zwave!
Open docker, go to /usr/src/balloob-python-openzwave/examples, run
python3 api_demo.py --device=/zwaveusbstick --log=Debug
2015-02-21 23:24:45 -08:00
Paulus Schoutsen
b5a3a72b51 Merge remote-tracking branch 'origin/master' into dev
* origin/master:
  Fix issue in Timer thread caused by variable name.
  Fix issue with some bulbs missing 'xy' attribute.
2015-02-21 18:24:10 -08:00
Paulus Schoutsen
6011cee490 Get python-openzwave working in Docker 2015-02-21 16:59:17 -08:00
Paulus Schoutsen
cfb7b8301c Merge pull request #35 from andythigpen/fix-thread-stop
Fix issue in Timer thread caused by variable name.
2015-02-21 16:00:30 -08:00
Paulus Schoutsen
69463308f5 Merge pull request #34 from andythigpen/fix-hue-xy
Fix issue with some bulbs missing 'xy' attribute.
2015-02-21 15:59:11 -08:00
andythigpen
8819aa7079 Fix issue in Timer thread caused by variable name.
threading.Thread already contains a _stop() function which is called in
threading._after_fork().  Assigning an Event object to _stop caused
periodic exceptions to be thrown.  This renames the _stop variable to
_stop_event to prevent a naming collision.
2015-02-21 15:21:43 -06:00
andythigpen
c3155651e4 Fix issue with some bulbs missing 'xy' attribute.
Philips Lux bulbs (as well as other dimmable bulbs that work with the
Hue hub) do not have an 'xy' attribute.
2015-02-21 15:02:08 -06:00
Paulus Schoutsen
24d9856ae6 More frontend code clean up 2015-02-20 00:23:09 -08:00
Paulus Schoutsen
89f59a758d Frontend: stream fires ping on connect
This is needed for Safari/Firefox to call onOpen
2015-02-19 23:31:06 -08:00
Paulus Schoutsen
ff4c3f791c Clean up frontend 2015-02-19 23:08:58 -08:00
Danielhiversen
8b590a43be Added documentation for heat control 2015-02-19 20:14:37 +01:00
Paulus Schoutsen
f46e0408b3 Add ping to streaming events API 2015-02-19 00:15:27 -08:00
Danielhiversen
a7c6413d07 Custom thermostat 2015-02-17 19:12:27 +01:00
Paulus Schoutsen
fd77e0e31d Upgraded home-assistant-js in frontend 2015-02-17 00:57:20 -08:00
Paulus Schoutsen
6119e1e660 Add a docker dev script 2015-02-17 00:56:35 -08:00
Paulus Schoutsen
d81723c8fc Frontend: handle modals in separate component 2015-02-15 21:53:40 -08:00
Paulus Schoutsen
b6a3524e9b Add a relative time component to frontend 2015-02-15 21:39:01 -08:00
Paulus Schoutsen
1d56181a8c Workaround Chrome Eventsource bug 2015-02-15 18:39:39 -08:00
Paulus Schoutsen
8e29910e77 Style fixes to satisfy updated Flake8 2015-02-15 17:15:46 -08:00
Paulus Schoutsen
37a9dbf1d5 Upgrade home-assistant.js version 2015-02-15 16:44:48 -08:00
Paulus Schoutsen
3ee2c6e210 Write to log if an event stream is closed 2015-02-15 16:44:25 -08:00
Paulus Schoutsen
c8cbb8ebb5 More info dialog plays nice again with History 2015-02-14 12:19:58 -08:00
Paulus Schoutsen
4047bf0775 Make more info dialog more store dependent 2015-02-14 10:05:26 -08:00
Paulus Schoutsen
faddb5d57e More frontend bugfixes 2015-02-14 01:06:51 -08:00
Paulus Schoutsen
3be8a1ad02 Notifications are now done via the new store 2015-02-14 00:11:46 -08:00
Paulus Schoutsen
dcffd102cc Migrate more components to use mixin 2015-02-14 00:11:23 -08:00
Paulus Schoutsen
791ebff7ee Squash some frontend bugs 2015-02-13 22:50:49 -08:00
Paulus Schoutsen
7dd7c489e8 Fire event for loading component/adding service 2015-02-13 22:49:56 -08:00
Paulus Schoutsen
846e11d6b8 Frontend streams events from HA 2015-02-13 18:59:42 -08:00
Paulus Schoutsen
3f26fc3b06 Move more HTTP stuff into constant file 2015-02-13 18:27:13 -08:00
Paulus Schoutsen
7a8f6500e2 Fix a bug in recorder. Fixes #33 2015-02-09 19:12:12 -08:00
Paulus Schoutsen
50bb4daeaa Added demo mode 2015-02-08 23:33:19 -08:00
Paulus Schoutsen
b643ef628b Merge branch 'dev'
* dev:
  Bug fixes related to entity_ids being lowercase
  Have statemachine.track_change work on new states
2015-02-08 22:28:28 -08:00
Paulus Schoutsen
ce39a6fb18 Bug fixes related to entity_ids being lowercase 2015-02-08 22:18:54 -08:00
Paulus Schoutsen
f5084a5f70 Have statemachine.track_change work on new states 2015-02-08 22:18:31 -08:00
Paulus Schoutsen
896aa31fdc Merge pull request #30 from issackelly/patch-2
Fix more links.
2015-02-08 12:59:24 -08:00
Issac Kelly
11b47063eb Fix more links.
Fix a couple more links. Jekyl doesn't appear to do automatic name= on headlines, so in-page linking isn't possible
2015-02-08 11:22:41 -08:00
Paulus Schoutsen
5ba752c331 Merge branch 'dev'
* dev:
  Enable history component instead of recorder by default
2015-02-07 22:52:41 -08:00
Paulus Schoutsen
6c4e044c92 Enable history component instead of recorder by default 2015-02-07 22:52:36 -08:00
Paulus Schoutsen
1d5a3b73f9 Merge branch 'dev'
* dev:
  Hide history view when history component not loaded
2015-02-07 22:17:35 -08:00
Paulus Schoutsen
8c20e5137e Hide history view when history component not loaded 2015-02-07 22:16:19 -08:00
Paulus Schoutsen
eea4bb7118 Merge branch 'dev'
* dev: (48 commits)
  Update README.md
  Fix some frontend bugs
  Fix style issues
  Move frontend specific js to webcomponents
  Improve performance for history component
  Fix JSON serialisation bug
  Allow querying recorder for run info
  Entity IDs are now always lowercase
  Get State History support from Store
  JS Stade model renamed entity_id to entityId
  Ensure entity ids are always lower case
  Period of history now returns a spanning result
  Tweaks for the new drawer UI
  Fixed Flake8 error
  Added a drawer to the UI
  Added suport for Tellstick light. Assume dimable switch is a light
  Added suport for Tellstick light. Assume dimable switch is a light
  Added suport for Tellstick light. Assume dimable switch is a light
  Update to latest home-assistant-js
  Frontend now build on top of home-assistant-js
  ...
2015-02-07 21:45:59 -08:00
Issac Kelly
1bc1cbd664 Update README.md
Fix Multiple Instances Link
2015-02-07 21:39:21 -08:00
Paulus Schoutsen
f8fc8c888f Fix some frontend bugs 2015-02-07 21:37:50 -08:00
Paulus Schoutsen
09bf64db42 Fix style issues 2015-02-07 15:54:58 -08:00
Paulus Schoutsen
33daf0a385 Move frontend specific js to webcomponents 2015-02-07 15:42:04 -08:00
Paulus Schoutsen
152ec80bfe Merge pull request #29 from issackelly/patch-1
Fix Multiple Instances Link
2015-02-07 15:30:24 -08:00
Issac Kelly
371b3775fa Update README.md
Fix Multiple Instances Link
2015-02-07 15:28:18 -08:00
Paulus Schoutsen
2cfcbf6380 Improve performance for history component 2015-02-07 13:23:01 -08:00
Paulus Schoutsen
5f0b3d0fca Fix JSON serialisation bug 2015-02-07 13:22:23 -08:00
Paulus Schoutsen
06fac90ec2 Allow querying recorder for run info 2015-02-07 12:39:10 -08:00
Paulus Schoutsen
d053f93419 Entity IDs are now always lowercase 2015-02-06 00:17:30 -08:00
Paulus Schoutsen
e3643b1faf Get State History support from Store 2015-02-06 00:02:40 -08:00
Paulus Schoutsen
029379c092 JS Stade model renamed entity_id to entityId 2015-02-06 00:01:29 -08:00
Paulus Schoutsen
36544ee088 Ensure entity ids are always lower case 2015-02-06 00:00:39 -08:00
Paulus Schoutsen
45dd8cbc3f Period of history now returns a spanning result 2015-02-05 22:57:03 -08:00
Paulus Schoutsen
772df97bc1 Tweaks for the new drawer UI 2015-02-05 22:36:41 -08:00
Paulus Schoutsen
0c181ead59 Merge pull request #27 from Danielhiversen/TellstickDimmer
Tellstick dimmer support
2015-02-05 10:10:03 -08:00
Danielhiversen
76d14157ec Fixed Flake8 error 2015-02-05 18:39:03 +01:00
Paulus Schoutsen
ce7b8b5e08 Added a drawer to the UI 2015-02-05 08:53:52 -08:00
Danielhiversen
cb9ad467af Added suport for Tellstick light. Assume dimable switch is a light 2015-02-04 22:54:05 +01:00
Danielhiversen
0ed8158f6e Added suport for Tellstick light. Assume dimable switch is a light 2015-02-04 22:33:04 +01:00
Danielhiversen
22c75f853b Added suport for Tellstick light. Assume dimable switch is a light 2015-02-04 22:29:30 +01:00
Paulus Schoutsen
b31668fba9 Update to latest home-assistant-js 2015-02-03 23:35:28 -08:00
Paulus Schoutsen
999e68812d Merge branch 'dev' of https://github.com/balloob/home-assistant into dev
* 'dev' of https://github.com/balloob/home-assistant:
  Fixed pylint errors
  fixed flake8 E302 expected 2 blank lines, found 1
  Make sure that components isn't loaded multiple times
  Refactered event listeners so that they can be part of a component instead of having to be stand alone components
2015-02-03 23:16:57 -08:00
Paulus Schoutsen
115be859b6 Frontend now build on top of home-assistant-js 2015-02-03 23:16:53 -08:00
Gustav Ahlberg
2e279d4722 Merge pull request #25 from balloob/component-scheduler
Component scheduler
2015-02-03 20:30:13 +01:00
Gustav Ahlberg
39485e7583 Fixed pylint errors 2015-02-03 20:18:19 +01:00
Gustav Ahlberg
ba7e06072d fixed flake8 E302 expected 2 blank lines, found 1 2015-02-02 07:39:11 +01:00
Gustav Ahlberg
7e9a254d87 Make sure that components isn't loaded multiple times 2015-02-02 07:30:46 +01:00
Paulus Schoutsen
fbae2ef725 Add Google Timelines to UI 2015-02-01 18:00:30 -08:00
Paulus Schoutsen
3439f4bb93 Remove frontend dependencies from git repo 2015-02-01 17:55:22 -08:00
Paulus Schoutsen
e2f51ef557 Default more info will show recent states 2015-01-31 20:07:04 -08:00
Paulus Schoutsen
807ceadf8b Provide API for 5 last states of entity 2015-01-31 20:06:30 -08:00
Paulus Schoutsen
3c95d80d3e Ensure recorder loaded first to capture all 2015-01-31 20:05:18 -08:00
Paulus Schoutsen
10bbc3d6e1 Default config: add recorder 2015-01-31 19:52:00 -08:00
Paulus Schoutsen
1ce8b6de7a Demo: show correct group name for living room 2015-01-31 19:51:42 -08:00
Paulus Schoutsen
c75447bc66 Expose loaded components on the API 2015-01-31 19:08:50 -08:00
Paulus Schoutsen
3709840327 Merge branch 'dev' into component-recorder
Conflicts:
	homeassistant/components/http/__init__.py
2015-01-31 10:31:16 -08:00
Paulus Schoutsen
cddeddac8d For backwards compat, have http load frontend/api 2015-01-31 09:55:26 -08:00
Paulus Schoutsen
6878fc254a Add frontend to default config 2015-01-30 08:28:04 -08:00
Paulus Schoutsen
13ac71bdf0 Clean up http related components 2015-01-30 08:26:06 -08:00
Paulus Schoutsen
61f6aff056 Upgrade to Polymer 0.5.4 2015-01-30 00:00:19 -08:00
Paulus Schoutsen
8feeafd8a3 Refactor http into frontend and api component 2015-01-29 23:56:21 -08:00
Gustav Ahlberg
6f3ef12d31 Refactered event listeners so that they can be part of a component instead of having to be stand alone components 2015-01-28 20:01:16 +01:00
Paulus Schoutsen
7c45318c00 Recorder component: proof of concept 2015-01-28 00:22:09 -08:00
Paulus Schoutsen
c2cd181c1a Merge pull request #11 from Gyran/scheduler
Scheduler component
2015-01-26 21:16:59 -08:00
Gustav Ahlberg
7066f25423 Fixed logging-format-interpolation lint errors 2015-01-25 21:50:54 +01:00
Gustav Ahlberg
631251f1f7 Merge remote-tracking branch 'upstream/master' into scheduler
* upstream/master: (104 commits)
  Fire a time_changed event every second
  Update example config with correct wink config
  Wink API is weird.  If you delete a device from their API, they dont delete it.  They just "hide" it
  Update the frontend with the new icon for sensor
  Minor refactor of build_frontend script to support linux, and not just mac
  Update script installs latest dependencies
  Fix flaky device scanner test
  Increased environment validation upon start
  Fix group names for switch, light and devices
  Disable pylint unused-argument check
  Fix device scanner test
  Better update schedules for cast and devicetracker
  Tweaks to the configurator UI
  Add tests, fix styling
  Add initial version of configurator component
  Fix tabs being selectable by clicking on header
  Data binding fix: Update instead of replace states
  New: State.last_updated represents creation date
  Update sensor icon for now
  Updates to resolve flake8 errors
  ...
2015-01-25 21:44:54 +01:00
Paulus Schoutsen
ade833a70a Fix automation example 2015-01-25 12:32:50 -08:00
Paulus Schoutsen
253e3eb628 Fire a time_changed event every second 2015-01-24 18:04:19 -08:00
Paulus Schoutsen
6588ca6520 Update example config with correct wink config 2015-01-22 00:29:47 -08:00
Paulus Schoutsen
4d0bd03569 Merge pull request #24 from kangaroo/dev
Minor fixes and cleanup
2015-01-21 23:00:59 -08:00
Geoff Norton
ed1c98e590 Wink API is weird. If you delete a device from their API, they dont delete it. They just "hide" it 2015-01-22 06:15:54 +00:00
Geoff Norton
8b947e2fab Merge remote-tracking branch 'origin/dev' into dev
Conflicts:
	homeassistant/components/http/frontend.py
	homeassistant/components/http/www_static/frontend.html
2015-01-22 05:27:26 +00:00
Geoff Norton
ead5d99394 Update the frontend with the new icon for sensor 2015-01-22 05:23:12 +00:00
Geoff Norton
98a5542cee Minor refactor of build_frontend script to support linux, and not just mac 2015-01-22 05:22:40 +00:00
Paulus Schoutsen
4f910beba4 Update script installs latest dependencies 2015-01-20 23:11:24 -08:00
Paulus Schoutsen
5b10d35aa8 Fix flaky device scanner test 2015-01-20 22:23:58 -08:00
Paulus Schoutsen
99c87ff862 Increased environment validation upon start 2015-01-20 22:18:44 -08:00
Paulus Schoutsen
dbefeb3f6b Fix group names for switch, light and devices 2015-01-19 23:47:18 -08:00
Paulus Schoutsen
ff230cefe3 Disable pylint unused-argument check 2015-01-19 23:40:51 -08:00
Paulus Schoutsen
d3f1b83e57 Fix device scanner test 2015-01-19 23:29:19 -08:00
Paulus Schoutsen
46834aa0a5 Better update schedules for cast and devicetracker 2015-01-19 22:57:28 -08:00
Paulus Schoutsen
2016da984a Tweaks to the configurator UI 2015-01-19 22:40:30 -08:00
Paulus Schoutsen
cdbcc844cf Add tests, fix styling 2015-01-19 21:40:54 -08:00
Paulus Schoutsen
980ecdaacb Add initial version of configurator component 2015-01-19 21:40:54 -08:00
Paulus Schoutsen
0c5f1234da Fix tabs being selectable by clicking on header 2015-01-19 21:39:44 -08:00
Paulus Schoutsen
35094a1667 Data binding fix: Update instead of replace states 2015-01-19 21:39:44 -08:00
Paulus Schoutsen
8d0bddac6c New: State.last_updated represents creation date 2015-01-19 21:39:44 -08:00
Paulus Schoutsen
9d933f517b Merge pull request #22 from kangaroo/wink-sensors
Wink sensor support
2015-01-19 21:30:44 -08:00
Geoff Norton
c224b91945 Merge branch 'wink-sensors' into dev 2015-01-20 05:22:39 +00:00
Geoff Norton
abfaca3bfb Update sensor icon for now 2015-01-20 04:55:12 +00:00
Geoff Norton
a287bb5da0 Updates to resolve flake8 errors 2015-01-20 04:23:31 +00:00
Geoff Norton
a7315684c4 Reduce the threshold on the sensor changes, and rename it for easier debugging 2015-01-20 04:00:31 +00:00
Geoff Norton
15012688f2 Remove switch state delay 2015-01-20 02:58:40 +00:00
Geoff Norton
78f7c3cbea Implement state for the sensor device 2015-01-20 02:58:21 +00:00
Geoff Norton
80c2d81f41 Fire the sensor timer more often, and fix a typo 2015-01-20 02:58:08 +00:00
Geoff Norton
454bea4237 Reduce the timer interval to make sensors useful 2015-01-20 02:57:50 +00:00
Geoff Norton
7fbd36ccad Add a new sensor component
Hook up to scan sensor_pod_id's from Wink and report their status.
2015-01-20 01:16:04 +00:00
Geoff Norton
ce9de1b7d2 Add initial support for wink sensors.
Refactor wink API fetching into a generic 'get_devices' function.
2015-01-20 01:14:47 +00:00
Paulus Schoutsen
869c4eb68d Update pynetgear to latest version 2015-01-19 08:43:29 -08:00
Paulus Schoutsen
dd38621352 Frontend: show message if no states 2015-01-17 23:29:46 -08:00
Paulus Schoutsen
5f0f06b22d Add command-line toggle to run in demo mode 2015-01-17 22:23:07 -08:00
Paulus Schoutsen
ed05ff6fd9 Allow for running Home Assistant without password 2015-01-17 21:55:33 -08:00
Paulus Schoutsen
50eecd11c1 Added command line toggle to open UI on start 2015-01-17 21:13:02 -08:00
Paulus Schoutsen
1f32ce2ca4 Put scripts in scripts folder 2015-01-17 14:32:33 -08:00
Paulus Schoutsen
45e295c1d3 Update pynetgear to latest version 2015-01-17 13:49:22 -08:00
Paulus Schoutsen
8562b8f09d Update to latest version netdisco 2015-01-16 08:52:19 -08:00
Paulus Schoutsen
9ffe35756b Component Automation - initial version 2015-01-15 23:32:27 -08:00
Paulus Schoutsen
93f93aff93 Merge branch 'dev'
* dev:
  Exclude external libraries from code coverage
  Bug fixes for Wink
  Better positioning of dialogs
  Added error handling in frontend debug forms
  Integrate add worker to bootstrap.setup_component
  Conditionally show widgets on light more info
  Rename the edit state button in more info to debug
  Expect devices to have no name
2015-01-15 21:39:43 -08:00
Paulus Schoutsen
db48b2ba34 Exclude external libraries from code coverage 2015-01-15 21:35:11 -08:00
Paulus Schoutsen
702498ca09 Bug fixes for Wink 2015-01-15 21:25:24 -08:00
Paulus Schoutsen
c116cb095d Better positioning of dialogs 2015-01-14 23:57:23 -08:00
Paulus Schoutsen
ced3d595cc Added error handling in frontend debug forms 2015-01-14 23:56:53 -08:00
Paulus Schoutsen
8c62ae4ce5 Integrate add worker to bootstrap.setup_component 2015-01-14 23:18:44 -08:00
Paulus Schoutsen
3e4c0261b1 Conditionally show widgets on light more info 2015-01-14 23:10:56 -08:00
Paulus Schoutsen
fd6ff4c8d3 Rename the edit state button in more info to debug 2015-01-14 22:15:27 -08:00
Paulus Schoutsen
7f7c403e00 Expect devices to have no name 2015-01-14 21:12:11 -08:00
Paulus Schoutsen
aa9673b208 Device Tracker sets up group auto attribute proper 2015-01-13 08:25:17 -08:00
Paulus Schoutsen
2fa4e2e468 Remove unused import from nest platform 2015-01-13 00:00:26 -08:00
Paulus Schoutsen
d1ad6f3e65 Show state attributes in default more info dialog 2015-01-12 23:57:57 -08:00
Paulus Schoutsen
b9a08bb25d Migrate nest platform to python-nest 2015-01-12 23:31:31 -08:00
Paulus Schoutsen
646f22bfbb Rename service_discovered to platform_discovered 2015-01-12 08:21:50 -08:00
Paulus Schoutsen
86e4fb9655 Merge branch 'component-wink' into dev
* component-wink:
  Wink component bug fixes
  Bugfixes to make Wink component work
  Added wink component to auto-load other needed components
  Initial version wink component by @loghound
  changed bearer_token to access_token in conf file
  Wink Hub integration of it's switches & lights

Conflicts:
	homeassistant/components/switch/__init__.py
	homeassistant/const.py
2015-01-11 23:22:40 -08:00
Paulus Schoutsen
19c474ba2f Remove TODO from nest platform 2015-01-11 21:38:03 -08:00
Paulus Schoutsen
8665f08dca Update netdisco to latest version 2015-01-11 21:23:22 -08:00
Paulus Schoutsen
aea6042fe1 Getting thermostat component ready for release 2015-01-11 21:21:18 -08:00
Paulus Schoutsen
0f8e282386 Wink component bug fixes 2015-01-11 14:23:24 -08:00
Paulus Schoutsen
9db1f3f8b7 Bugfixes to make Wink component work 2015-01-11 14:21:44 -08:00
Paulus Schoutsen
cac1f56b2d Merge branch 'dev' into component-thermostat
* dev:
  Use tuples instead of lists internally
  Use properties instead of getters for Device class
  Upgrade pushbullet.py to 0.7.1
  Prevent devices from being discovered twice
  Update netdisco to latest version
  Update netdisco to latest version
  Updated requirements.txt for the discovery component
  Automatic discovery and setting up of devices
  Ensure groups always have unique entity id
  Rename ha_test folder to tests
  Make group component more flexible
  Reorganized the main to be more modular
  Updated PyWemo to latest version
  Fix warnings from flake8 and pylint
  Check flags in ARP table for NUD_REACHABLE before assuming a device is online. Fixes #18.
  Pull in PyWemo bugfixes
2015-01-11 10:00:25 -08:00
Paulus Schoutsen
ca49a2aa68 Use tuples instead of lists internally 2015-01-11 09:55:45 -08:00
Paulus Schoutsen
283b187501 Use properties instead of getters for Device class 2015-01-11 09:20:41 -08:00
Paulus Schoutsen
035e3e686e Upgrade pushbullet.py to 0.7.1 2015-01-11 08:09:25 -08:00
Paulus Schoutsen
6cd53f2ddf Added wink component to auto-load other needed components 2015-01-10 23:47:23 -08:00
Paulus Schoutsen
249cf244ca Initial version wink component by @loghound 2015-01-10 22:53:41 -08:00
John McLaughlin
cd896627ea changed bearer_token to access_token in conf file 2015-01-10 22:35:14 -08:00
John McLaughlin
dccd9f562f Wink Hub integration of it's switches & lights 2015-01-10 22:35:01 -08:00
Paulus Schoutsen
c2b8f8d34e Prevent devices from being discovered twice 2015-01-10 10:34:56 -08:00
Paulus Schoutsen
9db1a58cb1 Update netdisco to latest version 2015-01-10 08:53:46 -08:00
Paulus Schoutsen
ebc67f460e Update netdisco to latest version 2015-01-09 22:22:19 -08:00
Paulus Schoutsen
e01fee9189 Updated requirements.txt for the discovery component 2015-01-09 00:13:24 -08:00
Paulus Schoutsen
ba179bc638 Automatic discovery and setting up of devices 2015-01-09 00:07:58 -08:00
Paulus Schoutsen
035d994705 Merge branch 'master' into dev
* master:
  Fix warnings from flake8 and pylint
  Check flags in ARP table for NUD_REACHABLE before assuming a device is online. Fixes #18.
2015-01-08 20:18:04 -08:00
Paulus Schoutsen
f1209a42a9 Ensure groups always have unique entity id 2015-01-08 20:17:05 -08:00
Paulus Schoutsen
d4cad0b267 Rename ha_test folder to tests 2015-01-08 20:05:12 -08:00
Paulus Schoutsen
e0b424c88f Make group component more flexible 2015-01-08 20:02:34 -08:00
Paulus Schoutsen
f5683797aa Reorganized the main to be more modular 2015-01-08 18:45:27 -08:00
Paulus Schoutsen
efe820628c Updated PyWemo to latest version 2015-01-06 20:52:26 -08:00
Paulus Schoutsen
300cfcc424 Merge pull request #19 from Piratonym/device_tracker-arpflags
device_tracker/openwrt: Check flags in ARP table for NUD_REACHABLE.
2015-01-06 19:49:38 -08:00
Karsten Nerdinger
2c5886f6d4 Fix warnings from flake8 and pylint 2015-01-07 03:57:06 +01:00
Karsten Nerdinger
fc33273464 Check flags in ARP table for NUD_REACHABLE before assuming a device is online. Fixes #18. 2015-01-07 03:36:39 +01:00
Paulus Schoutsen
cbbda2db55 Pull in PyWemo bugfixes 2015-01-06 07:49:49 -08:00
Paulus Schoutsen
6943a3bc14 Add a Nest specific card to frontend 2015-01-06 00:11:03 -08:00
Paulus Schoutsen
8f3a3f89a7 Refactor nest component into thermostat component 2015-01-06 00:10:33 -08:00
Paulus Schoutsen
68b712adfd Merge branch 'dev' into component-thermostat
* dev:
  Extracted a base HA device from ToggleDevice
2015-01-05 23:07:17 -08:00
Paulus Schoutsen
24be24c58b Extracted a base HA device from ToggleDevice 2015-01-05 23:02:41 -08:00
Paulus Schoutsen
db7004fdee Merge remote-tracking branch 'stefano/dev' into component-thermostat
* stefano/dev:
  Added "Nest" thermostat component.
2015-01-05 21:44:22 -08:00
Paulus Schoutsen
b10b75b7fe Reorganized frontend files 2015-01-05 21:41:15 -08:00
Paulus Schoutsen
ed3bbd98cc Remove unsupported specifying hosts for wemo 2015-01-05 21:35:22 -08:00
Paulus Schoutsen
ed1f434a61 Updated docker container instructions 2015-01-05 21:02:43 -08:00
Paulus Schoutsen
b90826c267 Better handling of subsequent throttle calls 2015-01-05 20:50:34 -08:00
Paulus Schoutsen
1b0143341c Update pywemo to latest version 2015-01-05 20:23:48 -08:00
sfiorini
b1a93ffc21 Added "Nest" thermostat component. 2015-01-05 19:11:02 -06:00
Paulus Schoutsen
b63784a4e6 Updated pywemo to latest version 2015-01-05 00:43:37 -08:00
Paulus Schoutsen
3894dec274 Default notify message title is now Home Assistant 2015-01-04 14:20:16 -08:00
Paulus Schoutsen
635876c94d Work around a bug in pylint 2015-01-04 01:34:48 -08:00
Paulus Schoutsen
490543093d Simple_alarm component notifies user on alarm 2015-01-04 01:24:25 -08:00
Paulus Schoutsen
4fec2dcb28 Add notification component and PushBullet platform 2015-01-04 01:14:18 -08:00
Paulus Schoutsen
a6ec071244 ps: dialogs on small screens only take needed height 2015-01-03 09:52:47 -08:00
Paulus Schoutsen
f64e84d087 State.last_changed only changes if state changes
No longer affected if just the attributes get updated.
2015-01-02 08:48:20 -08:00
Paulus Schoutsen
1ebaf7fd36 Tweaked the default 'more info' component text 2015-01-02 08:47:11 -08:00
Paulus Schoutsen
67d62a1723 New compiled version of frontend 2015-01-01 22:06:55 -08:00
Paulus Schoutsen
edb01b6bb4 Moved dialog logic to base class. 2015-01-01 22:06:39 -08:00
Paulus Schoutsen
5e9303dbf2 Create cards and more-info elements dynamically 2015-01-01 22:02:28 -08:00
Paulus Schoutsen
a0a1573dc9 More info for sun domain added 2015-01-01 21:02:30 -08:00
Paulus Schoutsen
debca88a0d All custom groups in 1 section instead of each their own 2015-01-01 20:03:24 -08:00
Paulus Schoutsen
dec12be52e Add more-info card for group domain 2015-01-01 19:42:20 -08:00
Paulus Schoutsen
85f5df55e9 frontend more-info-light: tweaks & cleanup 2014-12-30 18:37:33 -08:00
Paulus Schoutsen
fee51d604d demo comp: preserve color when turning on light that's on 2014-12-30 18:36:55 -08:00
Paulus Schoutsen
d506d0f424 Demo component now supports brightness/color for lights 2014-12-30 08:41:42 -08:00
Paulus Schoutsen
4dcaf12fa7 Added option to select light colors 2014-12-29 23:54:55 -08:00
Paulus Schoutsen
cfeb1f1538 Now able to change brightness of lights from more info dialog 2014-12-29 23:40:27 -08:00
Paulus Schoutsen
973525da6d Do not pollute template refs with ambigious names 2014-12-29 22:49:11 -08:00
Paulus Schoutsen
d5737aafce Frontend: Clicking a card opens more info screen 2014-12-29 22:47:29 -08:00
Gustav Ahlberg
b0b62d5db0 Fixed some lint errors 2014-12-29 01:18:02 +01:00
Gustav Ahlberg
03e30ea5ed SunEventListener documentation 2014-12-28 14:56:50 +01:00
Gustav Ahlberg
99b1cbf9b5 Merge remote-tracking branch 'upstream/master' into scheduler
* upstream/master: (60 commits)
  StateMachine is now case insensitive for entity ids
  Added an example component that does the bare minimum
  State card rendering now way more flexible
  Update README.md
  Update documentation for example component
  Add link to demo in README
  Add code to mock API for demo on home-assistant.io
  Moved documentation from GitHub source to home-assistant.io
  Make nmap mac regex more flexible to play nice with OS X
  Frontend: color switch icons yellow if on
  New strategy for defining number of used threads
  WeMo component exposes Insight info if available
  Only turn off the specified lights
  Fix default light and device group IDs
  Add nmap_tracker documentation
  Fix typo and default groups
  Specify devices for trigger
  nmap-based device tracking plugin
  Make block_till_stopped test more flexible
  Fix PyLint
  ...
2014-12-28 14:49:28 +01:00
Gustav Ahlberg
b1cc760bd1 TimeEventListener uses ServiceEventListener 2014-12-28 11:38:57 +01:00
Gustav Ahlberg
83320681f0 SunriseEventListener and SunsetEventListner 2014-12-28 11:37:37 +01:00
Gustav Ahlberg
c436b33da9 added seconds to timeEventListener 2014-12-28 11:29:26 +01:00
Paulus Schoutsen
89a548252a StateMachine is now case insensitive for entity ids 2014-12-26 23:26:39 -08:00
Paulus Schoutsen
df3ddea23c Added an example component that does the bare minimum 2014-12-26 22:58:58 -08:00
Paulus Schoutsen
029c38874b State card rendering now way more flexible
Decoupled state card into reusable components. Now able to have
different states be rendered with different state card components.
2014-12-23 21:12:42 -08:00
Paulus Schoutsen
5f7a9d9918 Update README.md 2014-12-21 23:42:21 -08:00
Paulus Schoutsen
da4ab542a7 Update documentation for example component 2014-12-21 22:21:32 -08:00
Paulus Schoutsen
0cfae7245b Add link to demo in README 2014-12-21 22:20:17 -08:00
Paulus Schoutsen
249d4d7062 Add code to mock API for demo on home-assistant.io 2014-12-21 15:07:01 -08:00
Paulus Schoutsen
e3961b7532 Moved documentation from GitHub source to home-assistant.io 2014-12-21 15:06:17 -08:00
Paulus Schoutsen
47e6290609 Make nmap mac regex more flexible to play nice with OS X 2014-12-19 15:42:34 -08:00
Paulus Schoutsen
47adae7917 Frontend: color switch icons yellow if on 2014-12-16 22:04:01 -08:00
Paulus Schoutsen
970014588a New strategy for defining number of used threads
Number of worker threads is 2 + 1 for each component that polls devices.
2014-12-16 21:46:02 -08:00
Paulus Schoutsen
528cd8ee48 WeMo component exposes Insight info if available 2014-12-15 19:14:31 -08:00
Paulus Schoutsen
7a9898fbd2 Merge remote-tracking branch 'origin/master' into dev 2014-12-15 18:57:53 -08:00
Paulus Schoutsen
c18bb7dcad Merge pull request #15 from trainman419/multi_room
Add support to device_sun_light_trigger to target specific device groups.
2014-12-15 18:57:12 -08:00
Paulus Schoutsen
bacad3b60e Merge pull request #14 from trainman419/nmap_tracker
nmap based device tracking plugin
2014-12-15 11:34:31 -08:00
trainman419
1c1d075c12 Only turn off the specified lights 2014-12-15 11:29:48 -08:00
trainman419
69a616a0ba Fix default light and device group IDs 2014-12-15 11:29:22 -08:00
trainman419
a49c839514 Add nmap_tracker documentation 2014-12-15 11:22:10 -08:00
trainman419
18396d2ee5 Fix typo and default groups 2014-12-15 00:49:56 -08:00
trainman419
cec5ca8ba2 Specify devices for trigger 2014-12-15 00:14:10 -08:00
trainman419
4495812b84 nmap-based device tracking plugin 2014-12-14 23:28:12 -08:00
Paulus Schoutsen
a54e6af24a Make block_till_stopped test more flexible 2014-12-14 19:59:52 -08:00
Paulus Schoutsen
bea7634b47 Fix PyLint 2014-12-14 19:59:37 -08:00
Paulus Schoutsen
344ce6ee4d Define number of worker threads based on cpu count 2014-12-14 18:28:11 -08:00
Paulus Schoutsen
4e4e6b1133 Upgrade to Polymer 0.5.2 2014-12-14 00:45:29 -08:00
Paulus Schoutsen
5e8673fc4a API Call Service returns states changed 2014-12-14 00:35:16 -08:00
Paulus Schoutsen
b091e9c31c Added TrackStates context manager 2014-12-14 00:32:20 -08:00
Paulus Schoutsen
dfa1e1c586 Fix Chromecast tests 2014-12-13 22:49:49 -08:00
Paulus Schoutsen
78d5625ace Calling a service can now block till execution is done 2014-12-13 22:40:00 -08:00
Paulus Schoutsen
f8223053bd Style fixes in home-assistant-api.html 2014-12-13 22:29:05 -08:00
Paulus Schoutsen
e2b434b24e Log file now also contains warnings 2014-12-12 08:15:34 -08:00
Paulus Schoutsen
7c404a0551 device_tracker - No longer keep writing same new devices to known_devices.csv 2014-12-12 07:32:50 -08:00
Paulus Schoutsen
81be3811dc Only save the valid entities in the group entity_ids attribute 2014-12-12 07:21:25 -08:00
Paulus Schoutsen
40cde1f836 Merge pull request #12 from jd/jd/typo
Fix typo in doc
2014-12-12 07:04:48 -08:00
Julien Danjou
36cb12cd15 Fix typo in doc 2014-12-12 11:40:20 +01:00
Paulus Schoutsen
df3521e706 Group component is more flexible when to setup a group 2014-12-11 17:31:01 -08:00
Paulus Schoutsen
ea1e4108cc Auth header for API is now prefixed with X- to follow spec 2014-12-09 00:34:36 -08:00
Paulus Schoutsen
756425f7b4 Added frontend icon for simple_alarm 2014-12-09 00:29:07 -08:00
Paulus Schoutsen
00e1ecb5ad Fix: simple_alarm will now report if proper initialized 2014-12-09 00:14:12 -08:00
Paulus Schoutsen
4e1b094449 Simple_alarm will only show known_alarm if that light is already on 2014-12-09 00:06:55 -08:00
Paulus Schoutsen
dd55d6c7f9 Added a simple_alarm component 2014-12-08 23:47:20 -08:00
Paulus Schoutsen
6044742cee Fix: Hue lights will not flash anymore on next turn on call after having flashed 2014-12-08 23:41:52 -08:00
Paulus Schoutsen
99447eaa17 Light component now supports sending flash command 2014-12-08 23:02:38 -08:00
Paulus Schoutsen
e7dff308ef Updated example component with more examples 2014-12-08 22:06:57 -08:00
Paulus Schoutsen
1f582cbeec Fix: Read known devices file once on init device_tracker 2014-12-08 22:06:38 -08:00
Paulus Schoutsen
68aa78d1fe Fix some style warnings by PyCharm 2014-12-07 01:28:52 -08:00
Paulus Schoutsen
0527760e9b Refactor: code moved to new helper and constants file. Also adds support for multiple types for switch/light components. 2014-12-06 23:57:02 -08:00
Paulus Schoutsen
513a03fb46 Updated API docs 2014-12-06 16:16:11 -08:00
Paulus Schoutsen
48089b01ab Renamed AddCooldown to Throttle and added bypass functionality 2014-12-04 21:06:45 -08:00
Paulus Schoutsen
31b9f65513 Added initial version of AddCooldown decorator 2014-12-04 01:14:27 -08:00
Gustav Ahlberg
c92089808f completed time_event_listener 2014-12-03 20:38:48 +01:00
Paulus Schoutsen
eef4817804 Cleaned up device_tracker and added tests 2014-12-02 23:54:10 -08:00
Paulus Schoutsen
12c734fa48 Update travis.yml to point at new test 2014-12-01 07:59:08 -08:00
Paulus Schoutsen
ed150b8ea5 Renamed test to ha_test because of conflict with built-in python test package 2014-11-30 23:15:18 -08:00
Paulus Schoutsen
5835d502c7 Moved more methods out of HomeAssistant object 2014-11-30 18:42:52 -08:00
Paulus Schoutsen
c08676aa81 Reorganized some core methods 2014-11-28 23:38:42 -08:00
Paulus Schoutsen
6f05548ec8 Add test coverage for demo component 2014-11-28 22:49:29 -08:00
Paulus Schoutsen
a4eb975b59 HTTP more robust and increased test coverage 2014-11-28 22:27:44 -08:00
Paulus Schoutsen
014abdba39 Remove unused imports in tests 2014-11-28 21:02:03 -08:00
Paulus Schoutsen
cdccdb432a Test remote methods for errors 2014-11-28 21:01:44 -08:00
Paulus Schoutsen
63f8f2ee7f Remote instance closes event forwarding on shutdown 2014-11-28 20:37:23 -08:00
Paulus Schoutsen
cfae4c667a Minor improvements to homeassistant.remote tests 2014-11-28 20:22:29 -08:00
Paulus Schoutsen
006310c883 Home Assistant stop is more robust 2014-11-28 20:22:08 -08:00
Paulus Schoutsen
89102b5652 Increase homeassistant.util test coverage 2014-11-28 19:42:16 -08:00
Paulus Schoutsen
5fe73cf33e Made dependency loading more robust 2014-11-28 15:36:21 -08:00
Gustav Ahlberg
09908f5780 Merge branch 'dev' of https://github.com/balloob/home-assistant into scheduler
# By Paulus Schoutsen
# Via Paulus Schoutsen
* 'dev' of https://github.com/balloob/home-assistant: (51 commits)
  Light test tests light profile loading
  Loader test tests now custom component loading
  Default config dir is now working_dir/config
  Add sun component test for state change
  Tweak light test to create correct exception
  Better light.xy_color parsing
  Added light component test coverage
  Renamed mock_switch_platform to mock_toggledevice_platform
  Expanded switch test to push it to 100% coverage
  Fix to make tests work on Travis CI
  Added tests for switch component
  Clean up code sun component tests
  Added test coverage for sun component
  Minor fix for Chromecast component
  Cleaned up tests a bit
  Added initial Chromecast test coverage
  Final test added to get to 100% coverage for groups
  Extended group tests
  Added group component tests
  Reorganized testing
  ...
2014-11-26 20:44:37 +01:00
Gustav Ahlberg
1c94bb1c0f Schedule component and time event
The schedule can read a schedule.json file and create time events
2014-11-26 20:38:40 +01:00
Paulus Schoutsen
ce1a5de607 Light test tests light profile loading 2014-11-25 23:16:07 -08:00
Paulus Schoutsen
c3047efc45 Loader test tests now custom component loading 2014-11-25 23:15:58 -08:00
Paulus Schoutsen
9c4111403e Default config dir is now working_dir/config 2014-11-25 23:15:18 -08:00
Paulus Schoutsen
cda04b7ece Add sun component test for state change 2014-11-25 22:31:36 -08:00
Paulus Schoutsen
4405d09d38 Tweak light test to create correct exception 2014-11-25 21:42:33 -08:00
Paulus Schoutsen
2220df5a3e Better light.xy_color parsing 2014-11-25 21:38:47 -08:00
Paulus Schoutsen
5f9787aeb2 Added light component test coverage 2014-11-25 21:28:43 -08:00
Paulus Schoutsen
845a028d42 Renamed mock_switch_platform to mock_toggledevice_platform 2014-11-25 19:16:42 -08:00
Paulus Schoutsen
3e348880d5 Expanded switch test to push it to 100% coverage 2014-11-25 00:33:02 -08:00
Paulus Schoutsen
100948eb38 Fix to make tests work on Travis CI 2014-11-25 00:28:14 -08:00
Paulus Schoutsen
5cbe7bf1b8 Added tests for switch component 2014-11-25 00:20:36 -08:00
Paulus Schoutsen
8c56b415cb Clean up code sun component tests 2014-11-24 23:35:50 -08:00
Paulus Schoutsen
4e155d50f3 Added test coverage for sun component 2014-11-24 23:19:33 -08:00
Paulus Schoutsen
470125b69a Minor fix for Chromecast component 2014-11-23 22:21:47 -08:00
Paulus Schoutsen
fa3b63f5e5 Cleaned up tests a bit 2014-11-23 22:18:51 -08:00
Paulus Schoutsen
244e2a0e7e Added initial Chromecast test coverage 2014-11-23 22:18:40 -08:00
Paulus Schoutsen
fa1b5b846e Final test added to get to 100% coverage for groups 2014-11-23 17:13:16 -08:00
Paulus Schoutsen
c6cb2c27bd Extended group tests 2014-11-23 17:06:19 -08:00
Paulus Schoutsen
a11ef38c9b Added group component tests 2014-11-23 16:46:59 -08:00
Paulus Schoutsen
bc4b81d525 Reorganized testing 2014-11-23 13:00:06 -08:00
Paulus Schoutsen
ad16c32504 Reorganized tests 2014-11-23 09:51:16 -08:00
Paulus Schoutsen
5278fe2f47 Added test coverage for core components 2014-11-23 00:32:03 -08:00
Paulus Schoutsen
ec59c3c793 Added test to validate frontend loads 2014-11-23 00:01:15 -08:00
Paulus Schoutsen
e5be72e445 Fix a flaky test 2014-11-22 23:41:47 -08:00
Paulus Schoutsen
296a5e3b10 Added test coverage badge to README 2014-11-22 23:36:11 -08:00
Paulus Schoutsen
38b85e3ca2 Added tests for loader and util 2014-11-22 23:35:59 -08:00
Paulus Schoutsen
b94ab32d60 Last pieces of test coverage for core classes 2014-11-22 22:37:53 -08:00
Paulus Schoutsen
5943f757a0 Even more test coverage 2014-11-22 21:40:01 -08:00
Paulus Schoutsen
1069d79298 All core classes have full test coverage 2014-11-22 18:17:36 -08:00
Paulus Schoutsen
2866437a1f remote.set_state would log error even if not 2014-11-22 17:16:23 -08:00
Paulus Schoutsen
a391bc3d3f Work around a pylint bug 2014-11-22 16:56:36 -08:00
Paulus Schoutsen
feeeac2a75 Remove unused __init__ file from external dir 2014-11-22 15:57:49 -08:00
Paulus Schoutsen
01f738c151 Move to Pypi version of Phue again 2014-11-22 15:49:54 -08:00
Paulus Schoutsen
4b2fa2d413 Added temporary Phue installation instructions 2014-11-20 23:10:25 -08:00
Paulus Schoutsen
d3bf245331 Updated documentation 2014-11-20 23:03:21 -08:00
Paulus Schoutsen
4fb2fcc7a0 ps: added comments to requirements.txt for clarity 2014-11-20 22:53:43 -08:00
Paulus Schoutsen
66f8daded1 Minor cleanup 2014-11-19 23:42:57 -08:00
Paulus Schoutsen
cc4c557e89 Cleanup of state-badge.html 2014-11-19 23:13:48 -08:00
Paulus Schoutsen
9656ff6636 show HA toolbar when showing popups for easy going back 2014-11-19 23:02:09 -08:00
Paulus Schoutsen
88c4c77cbd New compiled version frontend 2014-11-19 19:20:30 -08:00
Paulus Schoutsen
651d2dfd86 Improved layout of popups on small screens 2014-11-19 19:20:06 -08:00
Paulus Schoutsen
67bb64ab6b Cleaned up states-cards.html 2014-11-19 19:01:46 -08:00
Paulus Schoutsen
04e58bd375 tab bar autohides on scroll down 2014-11-19 19:01:26 -08:00
Paulus Schoutsen
a4dab870ce Reworked Splash Login. Hides virtual keyboard, squashed bugs. 2014-11-19 18:15:37 -08:00
Gustav Ahlberg
aab52ca686 Schedule component
Can read schedule json config file
Can load custom rule_types
2014-11-17 21:18:01 +01:00
Paulus Schoutsen
5d107ed74b Switched style checking to flake8 instead of pep8 2014-11-16 22:18:52 -08:00
Paulus Schoutsen
973ce21353 Improve flexibility of the loader 2014-11-14 23:17:18 -08:00
Paulus Schoutsen
0c7b6e26aa Add test coverage tracking via coveralls 2014-11-14 22:23:48 -08:00
Paulus Schoutsen
20ff5fadee Added doctype HTML to stop Safari converting selectors in embedded CSS to lower case 2014-11-14 00:26:20 -08:00
Paulus Schoutsen
bdfb8deb94 Updated screenshots 2014-11-13 22:52:39 -08:00
Paulus Schoutsen
fa75458b30 frontend compile script now strips comments 2014-11-13 22:47:06 -08:00
Paulus Schoutsen
cea18ee561 Minor frontend bugfixes 2014-11-13 22:25:43 -08:00
Paulus Schoutsen
2478656622 Upgraded to Polymer 0.5.1 2014-11-13 08:13:29 -08:00
Paulus Schoutsen
23d080af86 Upgraded to Polymer 0.5 2014-11-12 00:36:20 -08:00
Paulus Schoutsen
4dcdca1bd5 Updated CONTRIBUTING documentation 2014-11-11 22:51:08 -08:00
Paulus Schoutsen
2051871c81 Add pylint to travis 2014-11-11 22:18:35 -08:00
Paulus Schoutsen
da960b29da Added meta-tag theme-color for better app experience on Chrome Mobile 2014-11-11 22:04:26 -08:00
Paulus Schoutsen
9f9b926011 All platforms supported by components have their own file - you can can have custom platforms 2014-11-11 21:39:17 -08:00
Gustav Ahlberg
8c6e6e464e pylint and PEP8 errors 2014-11-11 18:59:38 -08:00
Gustav Ahlberg
f4e54719b9 Added support for tellstick devices and sensors 2014-11-11 18:59:38 -08:00
Paulus Schoutsen
c856c117a8 PEP8 style fix 2014-11-11 09:30:11 -08:00
Paulus Schoutsen
8ae9faf128 Added PEP8 checking to Travis 2014-11-11 09:14:00 -08:00
Paulus Schoutsen
4f226bcc7a Added CONTRIBUTING.md 2014-11-09 19:57:28 -08:00
Paulus Schoutsen
15f67fd6a7 Added a travis config file 2014-11-09 16:56:15 -08:00
Paulus Schoutsen
47dea785a8 Refactored light to be more reusable 2014-11-09 15:19:13 -08:00
Paulus Schoutsen
ca336bef57 Fixed a bug that prevented the service dialog from sending JSON 2014-11-09 14:45:43 -08:00
Paulus Schoutsen
a4bac63161 Cleanup of process component 2014-11-09 02:58:41 -08:00
Paulus Schoutsen
2ec1f20a03 Added support for unit_of_measurement 2014-11-08 17:45:09 -08:00
Paulus Schoutsen
5770cc03a1 Migrated wemo component to be part of a switch component 2014-11-08 17:20:43 -08:00
Paulus Schoutsen
222d57bda7 track_state_change now accepts a list of entity_ids 2014-11-08 14:22:17 -08:00
Paulus Schoutsen
1e136a2416 Logging is more efficient 2014-11-08 13:59:40 -08:00
Paulus Schoutsen
c3c1383ae6 More error checking added on start 2014-11-08 11:01:47 -08:00
Paulus Schoutsen
9f24101348 Refactor __main__ to use ArgParse 2014-11-05 07:58:20 -08:00
Paulus Schoutsen
aa80841519 Loader no longer crashes if custom_components folder does not exists in the config dir 2014-11-05 07:56:36 -08:00
Paulus Schoutsen
4c5bad495f Fix tests after enforcing entity id format 2014-11-05 07:46:33 -08:00
Paulus Schoutsen
1d90148f62 Updated the customizing section of the documentation 2014-11-05 07:21:07 -08:00
Paulus Schoutsen
a9ee2f9c54 Refactor: loading of components now done in a seperate module + better error reporting 2014-11-04 23:34:19 -08:00
Paulus Schoutsen
3c37f491b2 Update compiled frontend with latest changes 2014-11-04 20:24:48 -08:00
Paulus Schoutsen
ac2389a0a5 Responsive design now supports up to 4 columns 2014-11-04 20:23:46 -08:00
Paulus Schoutsen
714f747b61 Bugfix: Sun and light state icon default colors if on work again 2014-11-04 20:15:20 -08:00
Paulus Schoutsen
904bf4493e Added entity_id validation to the State class 2014-11-04 19:59:22 -08:00
Paulus Schoutsen
f9fbb30fc0 Updated screenshots 2014-11-02 20:54:00 -08:00
Paulus Schoutsen
c489c68f02 state-badge no longer sets url(undefined) as background-image 2014-11-02 18:50:16 -08:00
Paulus Schoutsen
bb23f57f96 Renamed build_polymer to build_frontend 2014-11-02 17:57:48 -08:00
Paulus Schoutsen
98c2f1ea42 Fix broken tests after API upgrade 2014-11-02 17:52:51 -08:00
Paulus Schoutsen
94d9cbf76e Starting home assistant is now done via __main__.py 2014-11-02 17:27:32 -08:00
Paulus Schoutsen
d56edd46bb Phue: Switched to GIT-version because pip version lacks behind 2014-11-02 16:22:59 -08:00
Paulus Schoutsen
542e6b9536 Timestamp tooltip is working again 2014-11-02 14:45:13 -08:00
Paulus Schoutsen
4f0b828a15 Responsive layout now grows up to 3 columns 2014-11-02 11:58:32 -08:00
Paulus Schoutsen
1bab576be7 Added support for entity pictures 2014-11-02 11:22:22 -08:00
Paulus Schoutsen
f9462613f5 Refactor Javascript backend 2014-11-02 10:45:49 -08:00
Paulus Schoutsen
ac8d70d547 Added a demo mode 2014-11-02 09:41:41 -08:00
Paulus Schoutsen
a4dd58cf5e Responsive happiness 2014-11-02 00:06:04 -07:00
Paulus Schoutsen
a0ab73882c No longer show groups on ALL tab 2014-11-01 22:10:32 -07:00
Paulus Schoutsen
48c3c44aba Updating compiled frontend to latest version 2014-11-01 15:37:24 -07:00
Paulus Schoutsen
523cd8249f Use friendly_name for entityDisplay if available 2014-11-01 15:29:39 -07:00
Paulus Schoutsen
b686f04121 Major cleanup frontend 2014-11-01 15:04:44 -07:00
Paulus Schoutsen
990ac057db Reduced font size of state card 2014-11-01 08:20:06 -07:00
Paulus Schoutsen
d1f3c84212 State card for light will show actual color of light 2014-11-01 08:16:34 -07:00
Paulus Schoutsen
86dc0a973c Sun will now also color yellow when above horizon 2014-10-31 00:23:06 -07:00
Paulus Schoutsen
ece0902ab2 after calling a service on a group, update all states 2014-10-31 00:21:29 -07:00
Paulus Schoutsen
895ba6adbc Light state badges will turn yellow when turned on 2014-10-31 00:15:22 -07:00
Paulus Schoutsen
7db8bc6423 lights will now show as on after being turned on 2014-10-30 23:55:38 -07:00
Paulus Schoutsen
68f8fd290a filtering by group no longer messes toggle for group state up 2014-10-30 23:01:13 -07:00
Paulus Schoutsen
22b3d7810d Default state name tweaks 2014-10-30 22:29:33 -07:00
Paulus Schoutsen
82b2b9cb94 Squashed bugs in frontend 2014-10-30 22:24:28 -07:00
Paulus Schoutsen
1d0657ff54 Frontend: header size matches material design 2014-10-29 19:04:03 -07:00
Paulus Schoutsen
30ba447c64 More material love for interface 2014-10-29 00:47:55 -07:00
Paulus Schoutsen
3bab3f4be1 Frontend password checking is now done in polymer 2014-10-28 00:38:25 -07:00
Paulus Schoutsen
b9d0316903 Updated Nexus 10 screenshot 2014-10-27 22:52:16 -07:00
Paulus Schoutsen
2e721f92a3 Updated README.md with new interface 2014-10-26 19:40:14 -07:00
Paulus Schoutsen
d2a4d67cb0 Added gzip and better caching 2014-10-26 18:10:01 -07:00
Paulus Schoutsen
506496743d Cleaning up the code 2014-10-24 23:44:00 -07:00
Paulus Schoutsen
3db43d6545 Now JavaScript is inlined too 2014-10-24 23:31:12 -07:00
Paulus Schoutsen
b4de063e76 Update docker file, node no longer installed 2014-10-24 23:10:29 -07:00
Paulus Schoutsen
e7c648a2c3 Home Assistant contains pre-compiled version of polymer components 2014-10-24 23:09:19 -07:00
Paulus Schoutsen
5596ac7d55 Chromecast no longer exposes host attribute 2014-10-24 23:09:19 -07:00
Paulus Schoutsen
450ce69353 Frontend now has mobile web app attribute set 2014-10-23 13:07:25 -07:00
Paulus Schoutsen
3eca37afd2 Added mobile device friendliness 2014-10-22 20:46:18 -07:00
Paulus Schoutsen
2a15e239c3 Improved state card 2014-10-22 20:28:16 -07:00
Paulus Schoutsen
552f78fc5c Added refresh button 2014-10-22 19:52:16 -07:00
Paulus Schoutsen
5d641b76d2 Add frontend build instruction to readme 2014-10-22 08:20:37 -07:00
Paulus Schoutsen
55f85f59d9 Fix the Dockerfile 2014-10-22 08:20:09 -07:00
Paulus Schoutsen
7eafa5805a In rare occasion the group component would not load 2014-10-22 08:12:32 -07:00
Paulus Schoutsen
f510ee333b group.setup now fails gracefully if config[group] not available. 2014-10-22 01:07:58 -07:00
Paulus Schoutsen
dc157edd7d Delete unused polymer file 2014-10-22 00:41:27 -07:00
Paulus Schoutsen
7cdda3a3d7 Group pep8 fix 2014-10-22 00:38:22 -07:00
Paulus Schoutsen
e8ab546d32 Added initial version of Polymer frontend 2014-10-22 00:38:13 -07:00
Paulus Schoutsen
80ecbe8057 Polymer build script added 2014-10-22 00:18:00 -07:00
Paulus Schoutsen
8a8097af99 Initial commit Polymer interface 2014-10-22 00:02:18 -07:00
Paulus Schoutsen
a0c12fe685 More API clean up 2014-10-21 23:52:24 -07:00
Paulus Schoutsen
e71efb3b68 pylint hints update 2014-10-21 23:52:05 -07:00
Paulus Schoutsen
c69b9aefec Bug fix: group not always loaded 2014-10-21 23:51:23 -07:00
Paulus Schoutsen
58c90402c5 Even more simplified API 2014-10-19 23:37:43 -07:00
Paulus Schoutsen
7c17987585 Updated API doc 2014-10-19 18:44:06 -07:00
Paulus Schoutsen
9979a3266e Call service api simplified 2014-10-19 18:41:06 -07:00
Paulus Schoutsen
001f27cdb4 HTTP API is now more RESTful 2014-10-17 00:17:02 -07:00
Paulus Schoutsen
951c3683b2 Added pylint configuration 2014-10-09 22:48:22 -07:00
Paulus Schoutsen
a339fd0b53 Fixed typo in architecture diagram 2014-10-04 13:29:50 -04:00
Paulus Schoutsen
8db1b74a3c device_sun_light_trigger: added option to disable turning devices off when all people gone 2014-09-27 07:18:55 -07:00
Paulus Schoutsen
8ef3009cc7 Better temporary phue Docker fix 2014-09-27 07:02:55 -07:00
Paulus Schoutsen
05e6ac8c1f Use git/master for phue while waiting for new release 2014-09-26 18:44:53 -07:00
Paulus Schoutsen
f06e4bde94 Documentation tweaks 2014-09-26 14:47:19 -07:00
Paulus Schoutsen
1ce48ece10 Updated documentation 2014-09-26 14:27:28 -07:00
Paulus Schoutsen
6164895bc9 Added PyUserinput to requirements.txt 2014-09-24 23:30:59 -05:00
Paulus Schoutsen
8abe473e53 Docker registry apparently does git clone --recursive 2014-09-24 23:18:10 -05:00
Paulus Schoutsen
a5976b7c6f Dockerfile initialized git submodules wrongly 2014-09-24 22:59:18 -05:00
Paulus Schoutsen
aa33e051bb Removed reference in gitsubmodule to pychromecast 2014-09-24 22:53:29 -05:00
Paulus Schoutsen
e8a5fa1413 Dockerfile integrated in main repo 2014-09-24 22:36:52 -05:00
Paulus Schoutsen
38ed025ce3 Improved logging message for HTTP and SUN components 2014-09-24 21:59:04 -05:00
Paulus Schoutsen
fbc1c21b2a Added requirements.txt 2014-09-24 21:34:09 -05:00
Paulus Schoutsen
27d448870d Tweak README.md layout for related projects 2014-09-24 12:06:12 -05:00
Paulus Schoutsen
29c62780fc Moved Tasker for Android to separate repository 2014-09-24 12:04:33 -05:00
Paulus Schoutsen
31c88efc4a Updated favicon 2014-09-23 16:51:39 -05:00
Paulus Schoutsen
17eefcffbe Home Assistant no longer crashes if it cannot write its log file 2014-09-22 21:44:26 -05:00
Paulus Schoutsen
d570aeef33 Configuration goes now into a single directory 2014-09-20 21:19:39 -05:00
Paulus Schoutsen
f24e9597fe Group component converts given entity ids to a list prior processing 2014-08-14 12:46:19 +02:00
Paulus Schoutsen
997c2e8ef6 Components+configuration now loaded dynamically
A major change to the bootstrapping of Home Assistant decoupling the
knowledge in bootstrap for a more dynamic approach. This refactoring
also prepares the code for different configuration backends and the
loading components from different places.
2014-08-13 14:28:45 +02:00
Paulus Schoutsen
cb33b3bf24 Added missing docstring 2014-06-13 22:11:05 -07:00
Paulus Schoutsen
2226f8b6a9 Debug interface: added domain icons to services 2014-06-13 22:00:43 -07:00
Paulus Schoutsen
c84cb86c87 Debug interface CSS pointed at wrong TDs 2014-06-13 14:41:58 -07:00
Paulus Schoutsen
00c206c37b Add domain icon for process in debug interface 2014-06-13 00:15:26 -07:00
Paulus Schoutsen
16453a7728 Added domain icons to debug interface 2014-06-13 00:12:05 -07:00
Paulus Schoutsen
8b02c3c1cc Updated PyNetgear and PyWemo to latest version 2014-06-12 23:26:06 -07:00
Paulus Schoutsen
2eebe7d91e Add timeouts to HTTP requests 2014-06-12 23:09:56 -07:00
Paulus Schoutsen
00f0890021 Updated README.md screenshots 2014-06-05 23:28:48 -07:00
Paulus Schoutsen
6e7887db23 PyLint style fixes 2014-06-05 23:12:00 -07:00
Paulus Schoutsen
b09fe4a3a7 HTTP Debug interface tweaks 2014-06-05 23:05:13 -07:00
Paulus Schoutsen
69882ff4cf Debug interface now has JS to help call services 2014-06-04 00:50:41 -07:00
Paulus Schoutsen
bb337fa0a9 Debug interface caches static assets 2014-06-04 00:49:46 -07:00
Paulus Schoutsen
e9d1dfac84 Validate API on starting remote instance 2014-05-01 23:03:14 -07:00
Paulus Schoutsen
50b492c64a Remote instances are now 100% operational 2014-04-29 00:30:31 -07:00
Paulus Schoutsen
8e65afa994 Add support to hardcode hosts of WeMos 2014-04-24 22:53:35 -07:00
Paulus Schoutsen
ea22695aa7 Updated example config with process config 2014-04-24 21:16:03 -07:00
Paulus Schoutsen
472308ec71 Optimized process component. 2014-04-24 20:43:33 -07:00
Paulus Schoutsen
b30aeb1777 Merge pull request #3 from fingon/process-source-up
Added a simple process state monitor
2014-04-24 14:47:07 -07:00
Paulus Schoutsen
2b071ad775 Merge pull request #2 from fingon/luci-checker
Added LuciDeviceScanner to scan OpenWrt Luci-RPC enabled router's state.
2014-04-24 14:44:51 -07:00
Markus Stenberg
88fd75b4c7 Addressed the comments in the issue. pep8+pylint seem fine now. (still works, too.) 2014-04-24 17:13:57 +03:00
Markus Stenberg
39d1fad8cc Merge remote-tracking branch 'up/master' into process-source-up 2014-04-24 17:00:46 +03:00
Markus Stenberg
9af3d2b914 pep8 + pylint run fixes. Also refactored the json-rpc to happen only in one place (external library would be nice but oh well). 2014-04-24 16:58:15 +03:00
Markus Stenberg
fc6c2fbc18 Merge remote-tracking branch 'up/master' into luci-checker 2014-04-24 16:31:03 +03:00
Paulus Schoutsen
2e10d7223a Re-organized core for better reusability 2014-04-24 00:40:45 -07:00
Markus Stenberg
e36a53eea6 Added simple process state monitor source. 2014-04-23 23:55:22 +03:00
Paulus Schoutsen
3757ddf9df Add option to set fixed hosts for chromecasts instead of discovery 2014-04-21 17:58:58 -07:00
Markus Stenberg
ed31b6a527 Added LuciDeviceScanner to scan OpenWrt Luci-RPC enabled router's state. 2014-04-21 22:51:50 +03:00
Paulus Schoutsen
f4c77c85bd Now using PyWemo instead of Ouimeaux to control WeMo 2014-04-17 21:57:01 -07:00
Paulus Schoutsen
85f401f209 Upgraded pynetgear to python3 2014-04-17 21:50:35 -07:00
Paulus Schoutsen
ef6d862671 Further Python 3 migration 2014-04-14 23:48:00 -07:00
Paulus Schoutsen
7e06d535ab Ported codebase to Python 3. Long Live Python 3! 2014-04-14 00:10:31 -07:00
241 changed files with 31234 additions and 5016 deletions

94
.coveragerc Normal file
View File

@@ -0,0 +1,94 @@
[run]
source = homeassistant
omit =
homeassistant/__main__.py
homeassistant/external/*
# omit pieces of code that rely on external devices being present
homeassistant/components/arduino.py
homeassistant/components/*/arduino.py
homeassistant/components/isy994.py
homeassistant/components/*/isy994.py
homeassistant/components/modbus.py
homeassistant/components/*/modbus.py
homeassistant/components/*/tellstick.py
homeassistant/components/*/vera.py
homeassistant/components/verisure.py
homeassistant/components/*/verisure.py
homeassistant/components/wink.py
homeassistant/components/*/wink.py
homeassistant/components/zwave.py
homeassistant/components/*/zwave.py
homeassistant/components/browser.py
homeassistant/components/camera/*
homeassistant/components/device_tracker/actiontec.py
homeassistant/components/device_tracker/asuswrt.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/luci.py
homeassistant/components/device_tracker/netgear.py
homeassistant/components/device_tracker/nmap_tracker.py
homeassistant/components/device_tracker/thomson.py
homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/tplink.py
homeassistant/components/discovery.py
homeassistant/components/downloader.py
homeassistant/components/keyboard.py
homeassistant/components/light/hue.py
homeassistant/components/light/limitlessled.py
homeassistant/components/media_player/cast.py
homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/squeezebox.py
homeassistant/components/notify/file.py
homeassistant/components/notify/instapush.py
homeassistant/components/notify/nma.py
homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushover.py
homeassistant/components/notify/slack.py
homeassistant/components/notify/smtp.py
homeassistant/components/notify/syslog.py
homeassistant/components/notify/xmpp.py
homeassistant/components/sensor/bitcoin.py
homeassistant/components/sensor/dht.py
homeassistant/components/sensor/efergy.py
homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/mysensors.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/rfxtrx.py
homeassistant/components/sensor/rpi_gpio.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/swiss_public_transport.py
homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/temper.py
homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/transmission.py
homeassistant/components/switch/command_switch.py
homeassistant/components/switch/edimax.py
homeassistant/components/switch/hikvisioncam.py
homeassistant/components/switch/rpi_gpio.py
homeassistant/components/switch/transmission.py
homeassistant/components/switch/wemo.py
homeassistant/components/thermostat/nest.py
[report]
# Regexes for lines to exclude from consideration
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover
# Don't complain about missing debug-only code:
def __repr__
# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError

21
.gitignore vendored
View File

@@ -1,6 +1,13 @@
home-assistant.log
home-assistant.conf
known_devices.csv
config/*
!config/home-assistant.conf.default
homeassistant/components/frontend/www_static/polymer/bower_components/*
# There is not a better solution afaik..
!config/custom_components
config/custom_components/*
!config/custom_components/example.py
!config/custom_components/hello_world.py
!config/custom_components/mqtt_example.py
# Hide sublime text stuff
*.sublime-project
@@ -56,4 +63,10 @@ nosetests.xml
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
.pydevproject
.python-version
# venv stuff
pyvenv.cfg
pip-selfcheck.json

18
.gitmodules vendored
View File

@@ -1,6 +1,12 @@
[submodule "homeassistant/external/pychromecast"]
path = homeassistant/external/pychromecast
url = https://github.com/balloob/pychromecast.git
[submodule "homeassistant/external/pynetgear"]
path = homeassistant/external/pynetgear
url = https://github.com/balloob/pynetgear.git
[submodule "homeassistant/external/noop"]
path = homeassistant/external/noop
url = https://github.com/balloob/noop.git
[submodule "homeassistant/external/vera"]
path = homeassistant/external/vera
url = https://github.com/jamespcole/home-assistant-vera-api.git
[submodule "homeassistant/external/nzbclients"]
path = homeassistant/external/nzbclients
url = https://github.com/jamespcole/home-assistant-nzb-clients.git
[submodule "homeassistant/components/frontend/www_static/home-assistant-polymer"]
path = homeassistant/components/frontend/www_static/home-assistant-polymer
url = https://github.com/balloob/home-assistant-polymer.git

13
.travis.yml Normal file
View File

@@ -0,0 +1,13 @@
sudo: false
language: python
python:
- "3.4"
install:
- pip install -r requirements_all.txt
- pip install flake8 pylint coveralls
script:
- flake8 homeassistant --exclude bower_components,external
- pylint homeassistant
- coverage run -m unittest discover tests
after_success:
- coveralls

72
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,72 @@
# Contributing to Home Assistant
Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them?
The process is straight-forward.
- Fork the Home Assistant [git repository](https://github.com/balloob/home-assistant).
- Write the code for your device, notification service, sensor, or IoT thing.
- Check it with ``pylint`` and ``flake8``.
- Create a Pull Request against the [**dev**](https://github.com/balloob/home-assistant/tree/dev) branch of Home Assistant.
Still interested? Then you should read the next sections and get more details.
## Adding support for a new device
For help on building your component, please see the [developer documentation](https://home-assistant.io/developers/) on [home-assistant.io](https://home-assistant.io/).
After you finish adding support for your device:
- Update the supported devices in the `README.md` file.
- Add any new dependencies to `requirements.txt`.
- 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`.
- Create a Pull Request against the [**dev**](https://github.com/balloob/home-assistant/tree/dev) branch of Home Assistant.
- Check for comments and suggestions on your Pull Request and keep an eye on the [Travis output](https://travis-ci.org/balloob/home-assistant/).
If you've added a component:
- Update the file [`home-assistant-icons.html`](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-icons.html) with an icon for your domain ([pick one from this list](https://www.polymer-project.org/1.0/components/core-elements/demo.html#core-icon)).
- Update the demo component with two states that it provides
- Add your component to home-assistant.conf.example
Since you've updated `home-assistant-icons.html`, you've made changes to the frontend:
- Run `build_frontend`. This will build a new version of the frontend. Make sure you add the changed files `frontend.py` and `frontend.html` to the commit.
### Setting states
It is the responsibility of the component to maintain the states of the devices in your domain. Each device should be a single state and, if possible, a group should be provided that tracks the combined state of the devices.
A state can have several attributes that will help the frontend in displaying your state:
- `friendly_name`: this name will be used as the name of the device
- `entity_picture`: this picture will be shown instead of the domain icon
- `unit_of_measurement`: this will be appended to the state in the interface
- `hidden`: This is a suggestion to the frontend on if the state should be hidden
These attributes are defined in [homeassistant.components](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/__init__.py#L25).
### Proper Visibility Handling
Generally, when creating a new entity for Home Assistant you will want it to be a class that inherits the [homeassistant.helpers.entity.Entity](https://github.com/balloob/home-assistant/blob/master/homeassistant/helpers/entity.py) class. If this is done, visibility will be handled for you.
You can set a suggestion for your entity's visibility by setting the hidden property by doing something similar to the following.
```python
self.hidden = True
```
This will SUGGEST that the active frontend hides the entity. This requires that the active frontend support hidden cards (the default frontend does) and that the value of hidden be included in your attributes dictionary (see above). The Entity abstract class will take care of this for you.
Remember: The suggestion set by your component's code will always be overwritten by user settings in the configuration.yaml file. This is why you may set hidden to be False, but the property may remain True (or vice-versa).
### Working on the frontend
The frontend is composed of [Polymer](https://www.polymer-project.org) web-components and compiled into the file `frontend.html`. During development you do not want to work with the compiled version but with the seperate files. To have Home Assistant serve the seperate files, set `development=1` for the *http-component* in your config.
When you are done with development and ready to commit your changes, run `build_frontend`, set `development=0` in your config and validate that everything still works.
### Notes on PyLint and PEP8 validation
In case a PyLint warning cannot be avoided, add a comment to disable the PyLint check for that line. This can be done using the format `# pylint: disable=YOUR-ERROR-NAME`. Example of an unavoidable PyLint warning is if you do not use the passed in datetime if you're listening for time change.

14
Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM python:3-onbuild
MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
VOLUME /config
RUN pip3 install --no-cache-dir -r requirements_all.txt
#RUN apt-get update && \
# apt-get install -y cython3 libudev-dev && \
# apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
# pip3 install cython && \
# scripts/build_python_openzwave
CMD [ "python", "-m", "homeassistant", "--config", "/config" ]

254
README.md
View File

@@ -1,240 +1,46 @@
Home Assistant
==============
# Home Assistant [![Build Status](https://travis-ci.org/balloob/home-assistant.svg?branch=master)](https://travis-ci.org/balloob/home-assistant) [![Coverage Status](https://img.shields.io/coveralls/balloob/home-assistant.svg)](https://coveralls.io/r/balloob/home-assistant?branch=master) [![Join the chat at https://gitter.im/balloob/home-assistant](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/balloob/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Home Assistant provides a platform for home automation. It does so by having modules that observe and trigger actors to do various tasks.
Home Assistant is a home automation platform running on Python 3. The goal of Home Assistant is to be able to track and control all devices at home and offer a platform for automating control. [Open a demo.](https://home-assistant.io/demo/)
It is currently able to do the following things:
* Track if devices are home by monitoring connected devices to a wireless router (currently supporting modern Netgear routers or routers running Tomato firmware)
* Track and control lights
* Track and control WeMo switches
* Track and control Chromecasts
* Turn on the lights when people get home when the sun is setting or has set
* Slowly turn on the lights to compensate for light loss when the sun sets and people are home
* Turn off lights and connected devices when everybody leaves the house
* Download files to the host machine
* Open a url in the default browser at the host machine
* Simulate key presses on the host for Play/Pause, Next track, Prev track, Volume up, Volume Down
* Controllable via a REST API and web interface
* Support for remoting Home Assistant instances through a Python API
* Android Tasker project to control Home Assistant from your phone and report charging state.
Check out [the website](https://home-assistant.io) for installation instructions, tutorials and documentation.
![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/states.png)
[![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png)](https://home-assistant.io/demo/)
Current compatible devices:
* [WeMo switches](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/)
* [Philips Hue](http://meethue.com)
* [Google Chromecast](http://www.google.com/intl/en/chrome/devices/chromecast)
* Wireless router running [Tomato firmware](http://www.polarcloud.com/tomato)
* Netgear wireless routers (tested with R6300)
Examples of devices it can interface it:
The system is built modular so support for other devices or actions can be implemented easily.
* 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, [Efergy](https://efergy.com) plugs, [Edimax](http://www.edimax.com/) switches, 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/)
* 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/)
Installation instructions
-------------------------
* The core depends on [PyEphem](http://rhodesmill.org/pyephem/) and [Requests](http://python-requests.org). Depending on the components you would like to use you will need [PHue](https://github.com/studioimaginaire/phue) for Philips Hue support, [PyChromecast](https://github.com/balloob/pychromecast) for Chromecast support and [ouimeaux](https://github.com/iancmcc/ouimeaux) for WeMo support. Install these using `pip install pyephem requests phue ouimeaux pychromecast`.
* Clone the repository and pull in the submodules `git clone --recursive https://github.com/balloob/home-assistant.git`
* Copy home-assistant.conf.default to home-assistant.conf and adjust the config values to match your setup.
* For Tomato you will have to not only setup your host, username and password but also a http_id. The http_id can be retrieved by going to the admin console of your router, view the source of any of the pages and search for `http_id`.
* If you want to use Hue, setup PHue by running `python -m phue --host HUE_BRIDGE_IP_ADDRESS` from the commandline and follow the instructions.
* While running the script it will create and maintain a file called `known_devices.csv` which will contain the detected devices. Adjust the track variable for the devices you want the script to act on and restart the script or call the service `device_tracker/reload_devices_csv`.
Built home automation on top of your devices:
Done. Start it now by running `python start.py`
* Keep a precise history of every change to the state of your house
* Turn on the lights when people get home after sun set
* Turn on lights slowly during sun set to compensate for less light
* Turn off all lights and devices when everybody leaves the house
* Offers a [REST API](https://home-assistant.io/developers/api.html) for easy integration with other projects
* Allow sending notifications using [Instapush](https://instapush.im), [Notify My Android (NMA)](http://www.notifymyandroid.com/), [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/), [Slack](https://slack.com/), and [Jabber (XMPP)](http://xmpp.org)
Web interface and API
---------------------
Home Assistent runs a webserver accessible on port 8123.
The system is built modular so support for other devices or actions can be implemented easily. See also the [section on architecture](https://home-assistant.io/developers/architecture.html) and the [section on creating your own components](https://home-assistant.io/developers/creating_components.html).
* At http://localhost:8123/ it will provide a debug interface showing the current state of the system and an overview of registered services.
* At http://localhost:8123/api/ it provides a password protected API.
If you run into issues while using Home Assistant or during development of a component, reach out to the [Home Assistant help section](https://home-assistant.io/help/) how to reach us.
A screenshot of the debug interface:
![screenshot-debug-interface](https://raw.github.com/balloob/home-assistant/master/docs/screenshot-debug-interface.png)
## Quick-start guide
All API calls have to be accompanied by an 'api_password' parameter (as specified in `home-assistant.conf`) and will
return JSON encoded objects. If successful calls will return status code 200 or 201.
Running Home Assistant requires [Python 3.4](https://www.python.org/). Run the following code to get up and running:
Other status codes that can occur are:
- 400 (Bad Request)
- 401 (Unauthorized)
- 404 (Not Found)
- 405 (Method not allowed)
The api supports the following actions:
**/api/states - GET**<br>
Returns a list of entity ids for which a state is available
```json
{
"entity_ids": [
"Paulus_Nexus_4",
"weather.sun",
"all_devices"
]
}
```
git clone --recursive https://github.com/balloob/home-assistant.git
python3 -m venv home-assistant
cd home-assistant
python3 -m homeassistant --open-ui
```
**/api/events - GET**<br>
Returns a dict with as keys the events and as value the number of listeners.
The last command will start the Home Assistant server and launch its web interface. By default Home Assistant looks for the configuration file `config/configuration.yaml`. A standard configuration file will be written if none exists.
```json
{
"event_listeners": {
"state_changed": 5,
"time_changed": 2
}
}
```
If you are still exploring if you want to use Home Assistant in the first place, you can enable the demo mode by adding the `--demo-mode` argument to the last command.
**/api/services - GET**<br>
Returns a dict with as keys the domain and as value a list of published services.
```json
{
"services": {
"browser": [
"browse_url"
],
"keyboard": [
"volume_up",
"volume_down"
]
}
}
```
**/api/states/&lt;entity_id>** - GET<br>
Returns the current state from an entity
```json
{
"attributes": {
"next_rising": "07:04:15 29-10-2013",
"next_setting": "18:00:31 29-10-2013"
},
"entity_id": "weather.sun",
"last_changed": "23:24:33 28-10-2013",
"state": "below_horizon"
}
```
**/api/states/&lt;entity_id>** - POST<br>
Updates the current state of an entity. Returns status code 201 if successful with location header of updated resource and the new state in the body.<br>
parameter: new_state - string<br>
optional parameter: attributes - JSON encoded object
```json
{
"attributes": {
"next_rising": "07:04:15 29-10-2013",
"next_setting": "18:00:31 29-10-2013"
},
"entity_id": "weather.sun",
"last_changed": "23:24:33 28-10-2013",
"state": "below_horizon"
}
```
**/api/events/&lt;event_type>** - POST<br>
Fires an event with event_type<br>
optional parameter: event_data - JSON encoded object
```json
{
"message": "Event download_file fired."
}
```
**/api/services/&lt;domain>/&lt;service>** - POST<br>
Calls a service within a specific domain.<br>
optional parameter: service_data - JSON encoded object
```json
{
"message": "Service keyboard/volume_up called."
}
```
Android remote control
----------------------
An app has been built using [Tasker for Android](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm) that:
* Provides buttons to control the lights and the chromecast
* Reports the charging state and battery level of the phone
The [APK](https://raw.github.com/balloob/home-assistant/master/android-tasker/Home_Assistant.apk) and [Tasker project XML](https://raw.github.com/balloob/home-assistant/master/android-tasker/Home_Assistant.prj.xml) can be found in [/android-tasker/](https://github.com/balloob/home-assistant/tree/master/android-tasker)
![screenshot-android-tasker.jpg](https://raw.github.com/balloob/home-assistant/master/docs/screenshot-android-tasker.png)
Architecture
------------
The core of Home Assistant exists of two parts; a Bus for calling services and firing events and a State Machine that keeps track of the state of things.
![screenshot-android-tasker.jpg](https://raw.github.com/balloob/home-assistant/master/docs/architecture.png)
For example to control the lights there are two components. One is the device_tracker that polls the wireless router for connected devices and updates the state of the tracked devices in the State Machine to be either 'Home' or 'Not Home'.
When a state is changed a state_changed event is fired for which the device_sun_light_trigger component is listening. Based on the new state of the device combined with the state of the sun it will decide if it should turn the lights on or off:
In the event that the state of device 'Paulus Nexus 5' changes to the 'Home' state:
If the sun has set and the lights are not on:
Turn on the lights
In the event that the combined state of all tracked devices changes to 'Not Home':
If the lights are on:
Turn off the lights
In the event of the sun setting:
If the lights are off and the combined state of all tracked device equals 'Home':
Turn on the lights
By using the Bus as a central communication hub between components it is easy to replace components or add functionality. For example if you would want to change the way devices are detected you only have to write a component that updates the device states in the State Machine.
### Components
**sun**
Tracks the state of the sun and when the next sun rising and setting will occur.
Depends on: latitude and longitude
Action: maintains state of `weather.sun` including attributes `next_rising` and `next_setting`
**device_tracker**
Keeps track of which devices are currently home.
Action: sets the state per device and maintains a combined state called `all_devices`. Keeps track of known devices in the file `known_devices.csv`.
**Light**
Keeps track which lights are turned on and can control the lights.
**WeMo**
Keeps track which WeMo switches are in the network and allows you to control them.
**device_sun_light_trigger**
Turns lights on or off using a light control component based on state of the sun and devices that are home.
Depends on: light control, track_sun, DeviceTracker
Action:
* Turns lights off when all devices leave home.
* Turns lights on when a device is home while sun is setting.
* Turns lights on when a device gets home after sun set.
Registers services `light_control/turn_light_on` and `light_control/turn_light_off` to turn a or all lights on or off.
Optional service data:
- `light_id` - only act on specific light. Else targets all.
- `transition_seconds` - seconds to take to swithc to new state.
**chromecast**
Registers three services to start playing YouTube video's on the ChromeCast.
Service `chromecast/play_youtube_video` starts playing the specified video on the YouTube app on the ChromeCast. Specify video using `video` in service_data.
Service `chromecast/start_fireplace` will start a YouTube movie simulating a fireplace and the `chromecast/start_epic_sax` service will start playing Epic Sax Guy 10h version.
**media_buttons**
Registers services that will simulate key presses on the keyboard. It currently offers the following Buttons as a Service (BaaS): `keyboard/volume_up`, `keyboard/volume_down` and `keyboard/media_play_pause`
This actor depends on: PyUserInput
**downloader**
Registers service `downloader/download_file` that will download files. File to download is specified in the `url` field in the service data.
**browser**
Registers service `browser/browse_url` that opens `url` as specified in event_data in the system default browser.
Please see [the getting started guide](https://home-assistant.io/getting-started/) on how to further configure Home Assistant.

Binary file not shown.

View File

@@ -1,871 +0,0 @@
<TaskerData sr="" dvi="1" tv="4.2u3m">
<dmetric>1080.0,1776.0</dmetric>
<Profile sr="prof25" ve="2">
<cdate>1380613730755</cdate>
<clp>true</clp>
<edate>1382769497429</edate>
<id>25</id>
<mid0>23</mid0>
<mid1>20</mid1>
<nme>HA Power USB</nme>
<pri>10</pri>
<State sr="con0">
<code>10</code>
<Int sr="arg0" val="2"/>
</State>
</Profile>
<Profile sr="prof26" ve="2">
<cdate>1380613730755</cdate>
<clp>true</clp>
<edate>1383003483161</edate>
<id>26</id>
<mid0>22</mid0>
<mid1>20</mid1>
<nme>HA Power Wireless</nme>
<pri>10</pri>
<State sr="con0">
<code>10</code>
<Int sr="arg0" val="3"/>
</State>
</Profile>
<Profile sr="prof3" ve="2">
<cdate>1380613730755</cdate>
<clp>true</clp>
<edate>1383003498566</edate>
<id>3</id>
<mid0>10</mid0>
<mid1>20</mid1>
<nme>HA Power AC</nme>
<pri>10</pri>
<State sr="con0">
<code>10</code>
<Int sr="arg0" val="1"/>
</State>
</Profile>
<Profile sr="prof5" ve="2">
<cdate>1380496514959</cdate>
<cldm>1500</cldm>
<clp>true</clp>
<edate>1382769618501</edate>
<id>5</id>
<mid0>19</mid0>
<nme>HA Battery Changed</nme>
<Event sr="con0" ve="2">
<code>203</code>
<pri>0</pri>
</Event>
</Profile>
<Project sr="proj0">
<cdate>1381110247781</cdate>
<name>Home Assistant</name>
<pids>3,25,26,5</pids>
<scenes>Variable Query,Home Assistant Start</scenes>
<tids>20,19,9,24,4,8,22,10,30,31,16,6,15,35,13,23,14,11,12,7,28,32,29</tids>
<Kid sr="Kid">
<launchID>12</launchID>
<pkg>nl.paulus.homeassistant</pkg>
<vnme>1.2</vnme>
<vnum>16</vnum>
</Kid>
<Img sr="icon" ve="2">
<nme>cust_animal_penguin</nme>
</Img>
</Project>
<Scene sr="sceneHome Assistant Start">
<cdate>1381113309678</cdate>
<edate>1381162068611</edate>
<heightLand>-1</heightLand>
<heightPort>688</heightPort>
<nme>Home Assistant Start</nme>
<widthLand>-1</widthLand>
<widthPort>523</widthPort>
<TextElement sr="elements0" ve="2">
<flags>4</flags>
<geom>0,17,523,107,-1,-1,-1,-1</geom>
<Str sr="arg0" ve="3">TextTitle</Str>
<Str sr="arg1" ve="3">Home Assistant</Str>
<Int sr="arg2" val="33"/>
<Int sr="arg3" val="100"/>
<Str sr="arg4" ve="3">#FFFFFFFF</Str>
<Int sr="arg5" val="0"/>
<Int sr="arg6" val="0"/>
<Int sr="arg7" val="0"/>
</TextElement>
<ListElement sr="elements1">
<flags>4</flags>
<geom>23,136,477,514,-1,-1,-1,-1</geom>
<itemclickTask>13</itemclickTask>
<Str sr="arg0" ve="3">Menu1</Str>
<Int sr="arg1" val="0"/>
<Str sr="arg2" ve="3"/>
<Int sr="arg3" val="0"/>
<Scene sr="arg4">
<Scene sr="val">
<cdate>1381113396824</cdate>
<edate>1381113396824</edate>
<heightLand>-1</heightLand>
<heightPort>100</heightPort>
<nme>Builtin Item Layout</nme>
<widthLand>-1</widthLand>
<widthPort>440</widthPort>
<ImageElement sr="elements0">
<flags>5</flags>
<geom>340,10,90,80,-1,-1,-1,-1</geom>
<Str sr="arg0" ve="3">Icon</Str>
<Img sr="arg1" ve="2">
<nme>hd_aaa_ext_tiles_small</nme>
</Img>
<Int sr="arg2" val="255"/>
</ImageElement>
<TextElement sr="elements1" ve="2">
<flags>5</flags>
<geom>60,10,270,80,-1,-1,-1,-1</geom>
<Str sr="arg0" ve="3">Label</Str>
<Str sr="arg1" ve="3"/>
<Int sr="arg2" val="18"/>
<Int sr="arg3"/>
<Str sr="arg4" ve="3">#FFFFFFFF</Str>
<Int sr="arg5" val="3"/>
<Int sr="arg6"/>
<Int sr="arg7"/>
</TextElement>
<TextElement sr="elements2" ve="2">
<flags>1</flags>
<geom>10,10,40,80,-1,-1,-1,-1</geom>
<Str sr="arg0" ve="3">Index</Str>
<Str sr="arg1" ve="3">1.</Str>
<Int sr="arg2" val="18"/>
<Int sr="arg3"/>
<Str sr="arg4" ve="3">#FFFFFFFF</Str>
<Int sr="arg5" val="3"/>
<Int sr="arg6"/>
<Int sr="arg7"/>
</TextElement>
<PropertiesElement sr="props">
<Int sr="arg0" val="1"/>
<Int sr="arg1" val="0"/>
<Str sr="arg2" ve="3">#00000000</Str>
<Int sr="arg3" val="0"/>
<Str sr="arg4" ve="3">Builtin Item Layout</Str>
<Str sr="arg5" ve="3"/>
<Img sr="arg6" ve="2"/>
<Str sr="arg7" ve="3"/>
</PropertiesElement>
</Scene>
</Scene>
<Int sr="arg5" val="1"/>
<Int sr="arg6" val="1"/>
<RectElement sr="background">
<flags>4</flags>
<geom>-1,-1,-1,-1,-1,-1,-1,-1</geom>
<Str sr="arg0" ve="3"/>
<Int sr="arg1" val="0"/>
<Str sr="arg2" ve="3">#77333333</Str>
<Str sr="arg3" ve="3">#77333333</Str>
<Int sr="arg4" val="0"/>
<Str sr="arg5" ve="3">#FF000000</Str>
<Int sr="arg6" val="0"/>
<Int sr="arg7" val="0"/>
</RectElement>
<ListElementItem sr="item0">
<label>Light On</label>
<Action sr="action" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">Light On</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3"/>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
<Img sr="icon" ve="2">
<nme>hd_aaa_ext_sun</nme>
</Img>
</ListElementItem>
<ListElementItem sr="item1">
<label>Light Off</label>
<Action sr="action" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">Light Off</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3"/>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
<Img sr="icon" ve="2">
<nme>hd_device_access_bightness_low</nme>
</Img>
</ListElementItem>
<ListElementItem sr="item2">
<label>Start Fireplace</label>
<Action sr="action" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">Start Fireplace</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3"/>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
<Img sr="icon" ve="2">
<nme>hd_aaa_ext_coffee</nme>
</Img>
</ListElementItem>
<ListElementItem sr="item3">
<label>Start Epic Sax</label>
<Action sr="action" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">Start Epic Sax</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3"/>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
<Img sr="icon" ve="2">
<nme>hd_aaa_ext_guitar</nme>
</Img>
</ListElementItem>
<ListElementItem sr="item4">
<label>Settings</label>
<Action sr="action" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">Setup</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3"/>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
<Img sr="icon" ve="2">
<nme>hd_action_settings</nme>
</Img>
</ListElementItem>
</ListElement>
<PropertiesElement sr="props">
<Int sr="arg0" val="1"/>
<Int sr="arg1" val="0"/>
<Str sr="arg2" ve="3">#DA000000</Str>
<Int sr="arg3" val="0"/>
<Str sr="arg4" ve="3">Home Assistant Start</Str>
<Str sr="arg5" ve="3"/>
<Img sr="arg6" ve="2"/>
<Str sr="arg7" ve="3"/>
</PropertiesElement>
</Scene>
<Scene sr="sceneVariable Query">
<cdate>1381112175910</cdate>
<edate>1381112254701</edate>
<heightLand>-1</heightLand>
<heightPort>380</heightPort>
<nme>Variable Query</nme>
<widthLand>-1</widthLand>
<widthPort>440</widthPort>
<TextElement sr="elements0" ve="2">
<flags>4</flags>
<geom>8,0,432,96,8,0,432,96</geom>
<Str sr="arg0" ve="3">Title</Str>
<Str sr="arg1" ve="3">Title</Str>
<Int sr="arg2" val="32"/>
<Int sr="arg3"/>
<Str sr="arg4" ve="3">#FF0099CC</Str>
<Int sr="arg5" val="3"/>
<Int sr="arg6"/>
<Int sr="arg7"/>
</TextElement>
<RectElement sr="elements1">
<flags>5</flags>
<geom>0,96,440,4,-1,-1,-1,-1</geom>
<Str sr="arg0" ve="3">Header</Str>
<Int sr="arg1" val="0"/>
<Str sr="arg2" ve="3">#77333333</Str>
<Str sr="arg3" ve="3">#77333333</Str>
<Int sr="arg4" val="0"/>
<Str sr="arg5" ve="3">#FF000000</Str>
<Int sr="arg6" val="0"/>
<Int sr="arg7" val="0"/>
</RectElement>
<EditTextElement sr="elements2">
<flags>13</flags>
<geom>20,156,400,96,-1,-1,-1,-1</geom>
<Str sr="arg0" ve="3">TextEdit1</Str>
<Str sr="arg1" ve="3"/>
<Int sr="arg2" val="16"/>
<Int sr="arg3" val="100"/>
<Str sr="arg4" ve="3">#FFFFFFFF</Str>
<Int sr="arg5" val="0"/>
<Int sr="arg6" val="0"/>
<Int sr="arg7" val="1000"/>
</EditTextElement>
<RectElement sr="elements3">
<flags>5</flags>
<geom>0,300,440,4,-1,-1,-1,-1</geom>
<Str sr="arg0" ve="3">Footer</Str>
<Int sr="arg1" val="0"/>
<Str sr="arg2" ve="3">#77333333</Str>
<Str sr="arg3" ve="3">#77333333</Str>
<Int sr="arg4" val="0"/>
<Str sr="arg5" ve="3">#FF000000</Str>
<Int sr="arg6" val="0"/>
<Int sr="arg7" val="0"/>
</RectElement>
<ImageElement sr="elements4">
<clickTask>-936</clickTask>
<flags>4</flags>
<geom>70,300,80,80,-1,-1,-1,-1</geom>
<Str sr="arg0" ve="3">Accept</Str>
<Img sr="arg1" ve="2">
<nme>hd_navigation_accept</nme>
</Img>
<Int sr="arg2" val="255"/>
</ImageElement>
<ImageElement sr="elements5">
<clickTask>-936</clickTask>
<flags>4</flags>
<geom>290,300,80,80,-1,-1,-1,-1</geom>
<Str sr="arg0" ve="3">Cancel</Str>
<Img sr="arg1" ve="2">
<nme>hd_content_remove</nme>
</Img>
<Int sr="arg2" val="255"/>
</ImageElement>
<PropertiesElement sr="props">
<Int sr="arg0" val="1"/>
<Int sr="arg1" val="0"/>
<Str sr="arg2" ve="3">#FF000000</Str>
<Int sr="arg3" val="0"/>
<Str sr="arg4" ve="3">Variable Query</Str>
<Str sr="arg5" ve="3"/>
<Img sr="arg6" ve="2"/>
<Str sr="arg7" ve="3"/>
</PropertiesElement>
</Scene>
<Task sr="task10">
<cdate>1380613530339</cdate>
<edate>1383030846230</edate>
<id>10</id>
<nme>Charging AC</nme>
<Action sr="act0" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">_Update Charging</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3">ac</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
</Task>
<Task sr="task11">
<cdate>1381110672417</cdate>
<edate>1384035370683</edate>
<id>11</id>
<nme>Open Debug Interface</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>104</code>
<Str sr="arg0" ve="3">http://%HA_HOST:%HA_PORT/?api_password=%HA_API_PASSWORD</Str>
</Action>
</Task>
<Task sr="task12">
<cdate>1381113015963</cdate>
<edate>1384219718372</edate>
<id>12</id>
<nme>Start Screen</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>47</code>
<Str sr="arg0" ve="3">Home Assistant Start</Str>
<Int sr="arg1" val="5"/>
<Int sr="arg2" val="100"/>
<Int sr="arg3" val="100"/>
<Int sr="arg4" val="1"/>
<Int sr="arg5" val="0"/>
<Int sr="arg6" val="0"/>
</Action>
<Action sr="act1" ve="3">
<code>49</code>
<Str sr="arg0" ve="3">Home Assistant Start</Str>
</Action>
<Img sr="icn" ve="2">
<nme>cust_animal_penguin</nme>
</Img>
</Task>
<Task sr="task13">
<cdate>1381114398467</cdate>
<edate>1381114398467</edate>
<id>13</id>
<pri>11</pri>
<Action sr="act0" ve="3">
<code>49</code>
<lhs>%tap_label</lhs>
<op>2</op>
<rhs>Settings</rhs>
<Str sr="arg0" ve="3">Home Assistant Start</Str>
</Action>
</Task>
<Task sr="task14">
<cdate>1381114829583</cdate>
<edate>1385537340259</edate>
<id>14</id>
<nme>API Fire Event</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>116</code>
<Str sr="arg0" ve="3">%HA_HOST:%HA_PORT</Str>
<Str sr="arg1" ve="3">/api/events/%par1</Str>
<Str sr="arg2" ve="3">api_password=%HA_API_PASSWORD</Str>
<Str sr="arg3" ve="3"/>
<Int sr="arg4" val="10"/>
<Str sr="arg5" ve="3"/>
<Str sr="arg6" ve="3"/>
</Action>
<Action sr="act1" ve="3">
<code>548</code>
<Str sr="arg0" ve="3">Fired event %par1</Str>
<Int sr="arg1" val="0"/>
</Action>
</Task>
<Task sr="task15">
<cdate>1380262442154</cdate>
<edate>1386787405570</edate>
<id>15</id>
<nme>Light On</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">API Call Service</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3">light/turn_on</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
<Img sr="icn" ve="2">
<nme>hd_aaa_ext_sun</nme>
</Img>
</Task>
<Task sr="task16">
<cdate>1380262442154</cdate>
<edate>1385172575157</edate>
<id>16</id>
<nme>Start Epic Sax</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">API Call Service</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3">chromecast/start_epic_sax</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
<Img sr="icn" ve="2">
<nme>hd_aaa_ext_guitar</nme>
</Img>
</Task>
<Task sr="task19">
<cdate>1380262442154</cdate>
<edate>1386695312804</edate>
<id>19</id>
<nme>Update Battery</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>547</code>
<label>Make sure charging var exist</label>
<lhs>%HA_CHARGING</lhs>
<op>10</op>
<rhs></rhs>
<Str sr="arg0" ve="3">%HA_CHARGING</Str>
<Str sr="arg1" ve="3">none</Str>
<Int sr="arg2" val="0"/>
<Int sr="arg3" val="0"/>
</Action>
<Action sr="act1" ve="3">
<code>116</code>
<Str sr="arg0" ve="3">%HA_HOST:%HA_PORT</Str>
<Str sr="arg1" ve="3">/api/states/devices.%HA_DEVICE_NAME.charging</Str>
<Str sr="arg2" ve="3">api_password=%HA_API_PASSWORD
new_state=%HA_CHARGING
attributes={"battery":%BATT}</Str>
<Str sr="arg3" ve="3"/>
<Int sr="arg4" val="10"/>
<Str sr="arg5" ve="3"/>
<Str sr="arg6" ve="3"/>
</Action>
</Task>
<Task sr="task20">
<cdate>1380613530339</cdate>
<edate>1386695398714</edate>
<id>20</id>
<nme>Charging None</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">_Update Charging</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3">none</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
</Task>
<Task sr="task22">
<cdate>1380613530339</cdate>
<edate>1383030909347</edate>
<id>22</id>
<nme>Charging Wireless</nme>
<Action sr="act0" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">_Update Charging</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3">wireless</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
</Task>
<Task sr="task23">
<cdate>1380613530339</cdate>
<edate>1383030849758</edate>
<id>23</id>
<nme>Charging USB</nme>
<Action sr="act0" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">_Update Charging</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3">usb</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
</Task>
<Task sr="task24">
<cdate>1381114829583</cdate>
<edate>1385537314797</edate>
<id>24</id>
<nme>API Call Service</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>116</code>
<Str sr="arg0" ve="3">%HA_HOST:%HA_PORT</Str>
<Str sr="arg1" ve="3">/api/services/%par1</Str>
<Str sr="arg2" ve="3">api_password=%HA_API_PASSWORD</Str>
<Str sr="arg3" ve="3"/>
<Int sr="arg4" val="10"/>
<Str sr="arg5" ve="3"/>
<Str sr="arg6" ve="3"/>
</Action>
<Action sr="act1" ve="3">
<code>548</code>
<Str sr="arg0" ve="3">Called service %par1</Str>
<Int sr="arg1" val="0"/>
</Action>
</Task>
<Task sr="task28">
<cdate>1384035383644</cdate>
<edate>1385172806993</edate>
<id>28</id>
<nme>Volume Down</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">API Call Service</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3">keyboard/volume_down</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
<Img sr="icn" ve="2">
<nme>hl_images_rotate_left</nme>
</Img>
</Task>
<Task sr="task29">
<cdate>1384035383644</cdate>
<edate>1385172552470</edate>
<id>29</id>
<nme>Play Pause</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">API Call Service</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3">keyboard/media_play_pause</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
<Img sr="icn" ve="2">
<nme>hl_av_pause</nme>
</Img>
</Task>
<Task sr="task30">
<cdate>1384035383644</cdate>
<edate>1385172803463</edate>
<id>30</id>
<nme>Volume Mute Toggle</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">API Call Service</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3">keyboard/volume_mute</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
<Img sr="icn" ve="2">
<nme>hl_device_access_volume_muted</nme>
</Img>
</Task>
<Task sr="task31">
<cdate>1384035383644</cdate>
<edate>1385172559562</edate>
<id>31</id>
<nme>Next Track</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">API Call Service</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3">keyboard/media_next_track</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
<Img sr="icn" ve="2">
<nme>hl_av_next</nme>
</Img>
</Task>
<Task sr="task32">
<cdate>1384035383644</cdate>
<edate>1385172567948</edate>
<id>32</id>
<nme>Prev Track</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">API Call Service</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3">keyboard/media_prev_track</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
<Img sr="icn" ve="2">
<nme>hl_av_previous</nme>
</Img>
</Task>
<Task sr="task35">
<cdate>1381114829583</cdate>
<edate>1385537324133</edate>
<id>35</id>
<nme>API Call Service With Data</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>116</code>
<Str sr="arg0" ve="3">%HA_HOST:%HA_PORT</Str>
<Str sr="arg1" ve="3">/api/services/%par1</Str>
<Str sr="arg2" ve="3">api_password=%HA_API_PASSWORD
service_data=%par2</Str>
<Str sr="arg3" ve="3"/>
<Int sr="arg4" val="10"/>
<Str sr="arg5" ve="3"/>
<Str sr="arg6" ve="3"/>
</Action>
<Action sr="act1" ve="3">
<code>548</code>
<Str sr="arg0" ve="3">Called service %par1</Str>
<Int sr="arg1" val="0"/>
</Action>
</Task>
<Task sr="task4">
<cdate>1380262442154</cdate>
<edate>1386787393520</edate>
<id>4</id>
<nme>Light Off</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">API Call Service</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3">light/turn_off</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
<Img sr="icn" ve="2">
<nme>hl_device_access_bightness_low</nme>
</Img>
</Task>
<Task sr="task6">
<cdate>1380522560890</cdate>
<edate>1383958813434</edate>
<id>6</id>
<nme>Setup</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>118</code>
<lhs>%HA_HOST</lhs>
<op>10</op>
<rhs></rhs>
<Str sr="arg0" ve="3">icanhazip.com</Str>
<Str sr="arg1" ve="3"/>
<Str sr="arg2" ve="3"/>
<Str sr="arg3" ve="3"/>
<Int sr="arg4" val="10"/>
<Str sr="arg5" ve="3"/>
<Str sr="arg6" ve="3">%HA_HOST</Str>
</Action>
<Action sr="act1" ve="3">
<code>547</code>
<lhs>%HA_HOST</lhs>
<op>10</op>
<rhs></rhs>
<Str sr="arg0" ve="3">%HA_HOST</Str>
<Str sr="arg1" ve="3">%HTTPD</Str>
<Int sr="arg2" val="0"/>
<Int sr="arg3" val="0"/>
</Action>
<Action sr="act2" ve="3">
<code>547</code>
<lhs>%HA_PORT</lhs>
<op>10</op>
<rhs></rhs>
<Str sr="arg0" ve="3">%HA_PORT</Str>
<Str sr="arg1" ve="3">8123</Str>
<Int sr="arg2" val="0"/>
<Int sr="arg3" val="0"/>
</Action>
<Action sr="act3" ve="3">
<code>547</code>
<lhs>%HA_API_PASSWORD</lhs>
<op>10</op>
<rhs></rhs>
<Str sr="arg0" ve="3">%HA_API_PASSWORD</Str>
<Str sr="arg1" ve="3">My password</Str>
<Int sr="arg2" val="0"/>
<Int sr="arg3" val="0"/>
</Action>
<Action sr="act4" ve="3">
<code>547</code>
<lhs>%HA_DEVICE_NAME</lhs>
<op>10</op>
<rhs></rhs>
<Str sr="arg0" ve="3">%HA_DEVICE_NAME</Str>
<Str sr="arg1" ve="3">%DEVMOD</Str>
<Int sr="arg2" val="0"/>
<Int sr="arg3" val="0"/>
</Action>
<Action sr="act5" ve="3">
<code>595</code>
<Str sr="arg0" ve="3">Host</Str>
<Str sr="arg1" ve="3">%HA_HOST</Str>
<Int sr="arg2" val="0"/>
<Str sr="arg3" ve="3">%HA_HOST</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3">Variable Query</Str>
<Int sr="arg6" val="40"/>
<Int sr="arg7" val="1"/>
</Action>
<Action sr="act6" ve="3">
<code>595</code>
<Str sr="arg0" ve="3">Port</Str>
<Str sr="arg1" ve="3">%HA_PORT</Str>
<Int sr="arg2" val="4"/>
<Str sr="arg3" ve="3">%HA_PORT</Str>
<Str sr="arg4" ve="3">%HA_PORT</Str>
<Str sr="arg5" ve="3">Variable Query</Str>
<Int sr="arg6" val="40"/>
<Int sr="arg7" val="1"/>
</Action>
<Action sr="act7" ve="3">
<code>595</code>
<Str sr="arg0" ve="3">API Password</Str>
<Str sr="arg1" ve="3">%HA_API_PASSWORD</Str>
<Int sr="arg2" val="0"/>
<Str sr="arg3" ve="3">%HA_API_PASSWORD</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3">Variable Query</Str>
<Int sr="arg6" val="40"/>
<Int sr="arg7" val="1"/>
</Action>
<Action sr="act8" ve="3">
<code>595</code>
<label>Ask device name</label>
<Str sr="arg0" ve="3">Device name</Str>
<Str sr="arg1" ve="3">%HA_DEVICE_NAME</Str>
<Int sr="arg2" val="0"/>
<Str sr="arg3" ve="3">%HA_DEVICE_NAME</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3">Variable Query</Str>
<Int sr="arg6" val="40"/>
<Int sr="arg7" val="1"/>
</Action>
<Img sr="icn" ve="2">
<nme>hd_ab_action_settings</nme>
</Img>
</Task>
<Task sr="task7">
<cdate>1384035383644</cdate>
<edate>1386787431769</edate>
<id>7</id>
<nme>Volume Up</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">API Call Service</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3">keyboard/volume_up</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
<Img sr="icn" ve="2">
<nme>hl_images_rotate_right</nme>
</Img>
</Task>
<Task sr="task8">
<cdate>1380262442154</cdate>
<edate>1386695263222</edate>
<id>8</id>
<nme>_Update Charging</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>547</code>
<Str sr="arg0" ve="3">%HA_CHARGING</Str>
<Str sr="arg1" ve="3">%par1</Str>
<Int sr="arg2" val="0"/>
<Int sr="arg3" val="0"/>
</Action>
<Action sr="act1" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">Update Battery</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3"/>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
</Task>
<Task sr="task9">
<cdate>1380262442154</cdate>
<edate>1386787379497</edate>
<id>9</id>
<nme>Start Fireplace</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">API Call Service</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3">chromecast/start_fireplace</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
<Img sr="icn" ve="2">
<nme>hd_aaa_ext_coffee</nme>
</Img>
</Task>
</TaskerData>

View File

@@ -0,0 +1,163 @@
homeassistant:
# Omitted values in this section will be auto detected using freegeoip.net
# Location required to calculate the time the sun rises and sets
latitude: 32.87336
longitude: 117.22743
# C for Celcius, F for Fahrenheit
temperature_unit: C
# Pick yours from here:
# http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
time_zone: America/Los_Angeles
# Name of the location where Home Assistant is running
name: Home
http:
api_password: mypass
# Set to 1 to enable development mode
# development: 1
light:
# platform: hue
wink:
# Get your token at https://winkbearertoken.appspot.com
access_token: 'YOUR_TOKEN'
device_tracker:
# The following types are available: ddwrt, netgear, tomato, luci,
# and nmap_tracker
platform: netgear
host: 192.168.1.1
username: admin
password: PASSWORD
# http_id is needed for Tomato routers only
# http_id: ABCDEFGHH
# For nmap_tracker, only the IP addresses to scan are needed:
# hosts: 192.168.1.1/24 # netmask prefix notation or
# hosts: 192.168.1.1-255 # address range
chromecast:
switch:
platform: wemo
thermostat:
platform: nest
# Required: username and password that are used to login to the Nest thermostat.
username: myemail@mydomain.com
password: mypassword
downloader:
download_dir: downloads
notify:
platform: pushbullet
api_key: ABCDEFGHJKLMNOPQRSTUVXYZ
device_sun_light_trigger:
# Optional: specify a specific light/group of lights that has to be turned on
light_group: group.living_room
# Optional: specify which light profile to use when turning lights on
light_profile: relax
# Optional: disable lights being turned off when everybody leaves the house
# disable_turn_off: 1
# 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)
group:
living_room:
- light.Bowl
- light.Ceiling
- light.TV_back_light
children:
- device_tracker.child_1
- device_tracker.child_2
process:
# items are which processes to look for: <entity_id>: <search string within ps>
xbmc: XBMC.App
example:
simple_alarm:
# Which light/light group has to flash when a known device comes home
known_light: light.Bowl
# Which light/light group has to flash red when light turns on while no one home
unknown_light: group.living_room
browser:
keyboard:
automation:
platform: state
alias: Sun starts shining
state_entity_id: sun.sun
# Next two are optional, omit to match all
state_from: below_horizon
state_to: above_horizon
execute_service: light.turn_off
service_entity_id: group.living_room
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!
sensor:
platform: systemmonitor
resources:
- type: 'disk_use_percent'
arg: '/'
- type: 'disk_use_percent'
arg: '/home'
- type: 'disk_use'
arg: '/home'
- type: 'disk_free'
arg: '/'
- type: 'memory_use_percent'
- type: 'memory_use'
- type: 'memory_free'
- type: 'processor_use'
- type: 'process'
arg: 'octave-cli'
script:
# Turns on the bedroom lights and then the living room lights 1 minute later
wakeup:
alias: Wake Up
sequence:
# alias is optional
- alias: Bedroom lights on
execute_service: light.turn_on
service_data:
entity_id: group.bedroom
- delay:
# supports seconds, milliseconds, minutes, hours, etc.
minutes: 1
- alias: Living room lights on
execute_service: light.turn_on
service_data:
entity_id: group.living_room
scene:
- name: Romantic
entities:
light.tv_back_light: on
light.ceiling:
state: on
xy_color: [0.33, 0.66]
brightness: 200

View File

@@ -0,0 +1,136 @@
"""
custom_components.example
~~~~~~~~~~~~~~~~~~~~~~~~~
Example component to target an entity_id to:
- turn it on at 7AM in the morning
- turn it on if anyone comes home and it is off
- turn it off if all lights are turned off
- turn it off if all people leave the house
- offer a service to turn it on for 10 seconds
Configuration:
To use the Example custom component you will need to add the following to
your config/configuration.yaml
example:
target: TARGET_ENTITY
Variable:
target
*Required
TARGET_ENTITY should be one of your devices that can be turned on and off,
ie a light or a switch. Example value could be light.Ceiling or switch.AC
(if you have these devices with those names).
"""
import time
import logging
from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_ON, STATE_OFF
import homeassistant.loader as loader
from homeassistant.helpers import validate_config
import homeassistant.components as core
# The domain of your component. Should be equal to the name of your component
DOMAIN = "example"
# List of component names (string) your component depends upon
# We depend on group because group will be loaded after all the components that
# initialize devices have been setup.
DEPENDENCIES = ['group']
# Configuration key for the entity id we are targetting
CONF_TARGET = 'target'
# Name of the service that we expose
SERVICE_FLASH = 'flash'
# Shortcut for the logger
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
""" Setup example component. """
# Validate that all required config options are given
if not validate_config(config, {DOMAIN: [CONF_TARGET]}, _LOGGER):
return False
target_id = config[DOMAIN][CONF_TARGET]
# Validate that the target entity id exists
if hass.states.get(target_id) is None:
_LOGGER.error("Target entity id %s does not exist", target_id)
# Tell the bootstrapper that we failed to initialize
return False
# We will use the component helper methods to check the states.
device_tracker = loader.get_component('device_tracker')
light = loader.get_component('light')
def track_devices(entity_id, old_state, new_state):
""" Called when the group.all devices change state. """
# If anyone comes home and the core is not on, turn it on.
if new_state.state == STATE_HOME and not core.is_on(hass, target_id):
core.turn_on(hass, target_id)
# If all people leave the house and the core is on, turn it off
elif new_state.state == STATE_NOT_HOME and core.is_on(hass, target_id):
core.turn_off(hass, target_id)
# Register our track_devices method to receive state changes of the
# all tracked devices group.
hass.states.track_change(
device_tracker.ENTITY_ID_ALL_DEVICES, track_devices)
def wake_up(now):
""" Turn it on in the morning if there are people home and
it is not already on. """
if device_tracker.is_on(hass) and not core.is_on(hass, target_id):
_LOGGER.info('People home at 7AM, turning it on')
core.turn_on(hass, target_id)
# Register our wake_up service to be called at 7AM in the morning
hass.track_time_change(wake_up, hour=7, minute=0, second=0)
def all_lights_off(entity_id, old_state, new_state):
""" If all lights turn off, turn off. """
if core.is_on(hass, target_id):
_LOGGER.info('All lights have been turned off, turning it off')
core.turn_off(hass, target_id)
# Register our all_lights_off method to be called when all lights turn off
hass.states.track_change(
light.ENTITY_ID_ALL_LIGHTS, all_lights_off, STATE_ON, STATE_OFF)
def flash_service(call):
""" Service that will turn the target off for 10 seconds
if on and vice versa. """
if core.is_on(hass, target_id):
core.turn_off(hass, target_id)
time.sleep(10)
core.turn_on(hass, target_id)
else:
core.turn_on(hass, target_id)
time.sleep(10)
core.turn_off(hass, target_id)
# Register our service with HASS.
hass.services.register(DOMAIN, SERVICE_FLASH, flash_service)
# Tells the bootstrapper that the component was successfully initialized
return True

View File

@@ -0,0 +1,30 @@
"""
custom_components.hello_world
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Implements the bare minimum that a component should implement.
Configuration:
To use the hello_word component you will need to add the following to your
config/configuration.yaml
hello_world:
"""
# The domain of your component. Should be equal to the name of your component
DOMAIN = "hello_world"
# List of component names (string) your component depends upon
DEPENDENCIES = []
def setup(hass, config):
""" Setup our skeleton component. """
# States are in the format DOMAIN.OBJECT_ID
hass.states.set('hello_world.Hello_World', 'Works!')
# return boolean to indicate that initialization was successful
return True

View File

@@ -0,0 +1,60 @@
"""
custom_components.mqtt_example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Shows how to communicate with MQTT. Follows a topic on MQTT and updates the
state of an entity to the last message received on that topic.
Also offers a service 'set_state' that will publish a message on the topic that
will be passed via MQTT to our message received listener. Call the service with
example payload {"new_state": "some new state"}.
Configuration:
To use the mqtt_example component you will need to add the following to your
config/configuration.yaml
mqtt_example:
topic: home-assistant/mqtt_example
"""
import homeassistant.loader as loader
# The domain of your component. Should be equal to the name of your component
DOMAIN = "mqtt_example"
# List of component names (string) your component depends upon
DEPENDENCIES = ['mqtt']
CONF_TOPIC = 'topic'
DEFAULT_TOPIC = 'home-assistant/mqtt_example'
def setup(hass, config):
""" Setup our mqtt_example component. """
mqtt = loader.get_component('mqtt')
topic = config[DOMAIN].get('topic', DEFAULT_TOPIC)
entity_id = 'mqtt_example.last_message'
# Listen to a message on MQTT
def message_received(topic, payload, qos):
""" A new MQTT message has been received. """
hass.states.set(entity_id, payload)
mqtt.subscribe(hass, topic, message_received)
hass.states.set(entity_id, 'No messages')
# Service to publish a message on MQTT
def set_state_service(call):
""" Service to send a message. """
mqtt.publish(hass, topic, call.data.get('new_state'))
# Register our service with Home Assistant
hass.services.register(DOMAIN, 'set_state', set_state_service)
# return boolean to indicate that initialization was successful
return True

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 453 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

BIN
docs/screenshots.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

View File

@@ -1,39 +0,0 @@
[common]
latitude=32.87336
longitude=-117.22743
[httpinterface]
api_password=mypass
[light.hue]
host=192.168.1.2
[device_tracker.tomato]
host=192.168.1.1
username=admin
password=PASSWORD
http_id=aaaaaaaaaaaaaaa
[device_tracker.netgear]
host=192.168.1.1
username=admin
password=PASSWORD
[chromecast]
[wemo]
[downloader]
download_dir=downloads
[device_sun_light_trigger]
# Example how you can specify a specific group that has to be turned on
# light_group=group.living_room
# Example how you can specify which light profile to use when turning lights on
# light_profile=relax
# A comma seperated list of states that have to be tracked
# As a single group
[group]
living_room=light.Bowl,light.Ceiling,light.TV_back_light
bedroom=light.Bed_light

View File

@@ -1,577 +0,0 @@
"""
homeassistant
~~~~~~~~~~~~~
Home Assistant is a Home Automation framework for observing the state
of entities and react to changes.
"""
import time
import logging
import threading
import datetime as dt
import functools as ft
import homeassistant.util as util
MATCH_ALL = '*'
DOMAIN = "homeassistant"
SERVICE_HOMEASSISTANT_STOP = "stop"
EVENT_HOMEASSISTANT_START = "homeassistant_start"
EVENT_STATE_CHANGED = "state_changed"
EVENT_TIME_CHANGED = "time_changed"
TIMER_INTERVAL = 10 # seconds
# We want to be able to fire every time a minute starts (seconds=0).
# We want this so other modules can use that to make sure they fire
# every minute.
assert 60 % TIMER_INTERVAL == 0, "60 % TIMER_INTERVAL should be 0!"
BUS_NUM_THREAD = 4
BUS_REPORT_BUSY_TIMEOUT = dt.timedelta(minutes=1)
PRIO_SERVICE_DEFAULT = 1
PRIO_EVENT_STATE = 2
PRIO_EVENT_TIME = 3
PRIO_EVENT_DEFAULT = 4
def start_home_assistant(bus):
""" Start home assistant. """
request_shutdown = threading.Event()
bus.register_service(DOMAIN, SERVICE_HOMEASSISTANT_STOP,
lambda service: request_shutdown.set())
Timer(bus)
bus.fire_event(EVENT_HOMEASSISTANT_START)
while not request_shutdown.isSet():
try:
time.sleep(1)
except KeyboardInterrupt:
break
def _process_match_param(parameter):
""" Wraps parameter in a list if it is not one and returns it. """
if not parameter:
return MATCH_ALL
elif isinstance(parameter, list):
return parameter
else:
return [parameter]
def _matcher(subject, pattern):
""" Returns True if subject matches the pattern.
Pattern is either a list of allowed subjects or a `MATCH_ALL`.
"""
return MATCH_ALL == pattern or subject in pattern
def track_state_change(bus, entity_id, action, from_state=None, to_state=None):
""" Helper method to track specific state changes. """
from_state = _process_match_param(from_state)
to_state = _process_match_param(to_state)
@ft.wraps(action)
def state_listener(event):
""" State change listener that listens for specific state changes. """
if entity_id == event.data['entity_id'] and \
_matcher(event.data['old_state'].state, from_state) and \
_matcher(event.data['new_state'].state, to_state):
action(event.data['entity_id'],
event.data['old_state'],
event.data['new_state'])
bus.listen_event(EVENT_STATE_CHANGED, state_listener)
def track_point_in_time(bus, action, point_in_time):
""" Adds a listener that will fire once after a spefic point in time. """
@ft.wraps(action)
def point_in_time_listener(event):
""" Listens for matching time_changed events. """
now = event.data['now']
if now > point_in_time and not hasattr(point_in_time_listener, 'run'):
# Set variable so that we will never run twice.
# Because the event bus might have to wait till a thread comes
# available to execute this listener it might occur that the
# listener gets lined up twice to be executed. This will make sure
# the second time it does nothing.
point_in_time_listener.run = True
bus.remove_event_listener(EVENT_TIME_CHANGED,
point_in_time_listener)
action(now)
bus.listen_event(EVENT_TIME_CHANGED, point_in_time_listener)
# pylint: disable=too-many-arguments
def track_time_change(bus, action,
year=None, month=None, day=None,
hour=None, minute=None, second=None):
""" Adds a listener that will fire if time matches a pattern. """
# We do not have to wrap the function with time pattern matching logic if
# no pattern given
if any((val is not None for val in
(year, month, day, hour, minute, second))):
pmp = _process_match_param
year, month, day = pmp(year), pmp(month), pmp(day)
hour, minute, second = pmp(hour), pmp(minute), pmp(second)
@ft.wraps(action)
def time_listener(event):
""" Listens for matching time_changed events. """
now = event.data['now']
mat = _matcher
if mat(now.year, year) and \
mat(now.month, month) and \
mat(now.day, day) and \
mat(now.hour, hour) and \
mat(now.minute, minute) and \
mat(now.second, second):
action(now)
else:
@ft.wraps(action)
def time_listener(event):
""" Fires every time event that comes in. """
action(event.data['now'])
bus.listen_event(EVENT_TIME_CHANGED, time_listener)
def create_bus_job_handler(logger):
""" Creates a job handler that logs errors to supplied `logger`. """
def job_handler(job):
""" Called whenever a job is available to do. """
try:
func, arg = job
func(arg)
except Exception: # pylint: disable=broad-except
# Catch any exception our service/event_listener might throw
# We do not want to crash our ThreadPool
logger.exception(u"BusHandler:Exception doing job")
return job_handler
# pylint: disable=too-few-public-methods
class ServiceCall(object):
""" Represents a call to a service. """
__slots__ = ['domain', 'service', 'data']
def __init__(self, domain, service, data=None):
self.domain = domain
self.service = service
self.data = data or {}
def __repr__(self):
if self.data:
return u"<ServiceCall {}.{}: {}>".format(
self.domain, self.service, util.repr_helper(self.data))
else:
return u"<ServiceCall {}.{}>".format(self.domain, self.service)
# pylint: disable=too-few-public-methods
class Event(object):
""" Represents an event within the Bus. """
__slots__ = ['event_type', 'data']
def __init__(self, event_type, data=None):
self.event_type = event_type
self.data = data or {}
def __repr__(self):
if self.data:
return u"<Event {}: {}>".format(
self.event_type, util.repr_helper(self.data))
else:
return u"<Event {}>".format(self.event_type)
class Bus(object):
""" Class that allows different components to communicate via services
and events.
"""
# pylint: disable=too-many-instance-attributes
def __init__(self, thread_count=None):
self.thread_count = thread_count or BUS_NUM_THREAD
self._event_listeners = {}
self._services = {}
self.logger = logging.getLogger(__name__)
self.event_lock = threading.Lock()
self.service_lock = threading.Lock()
self.last_busy_notice = dt.datetime.now()
self.pool = util.ThreadPool(self.thread_count,
create_bus_job_handler(self.logger))
@property
def services(self):
""" Dict with per domain a list of available services. """
with self.service_lock:
return {domain: self._services[domain].keys()
for domain in self._services}
@property
def event_listeners(self):
""" Dict with events that is being listened for and the number
of listeners.
"""
with self.event_lock:
return {key: len(self._event_listeners[key])
for key in self._event_listeners}
def has_service(self, domain, service):
""" Returns True if specified service exists. """
try:
return service in self._services[domain]
except KeyError: # if key 'domain' does not exist
return False
def call_service(self, domain, service, service_data=None):
""" Calls a service. """
service_call = ServiceCall(domain, service, service_data)
with self.service_lock:
try:
self.pool.add_job(PRIO_SERVICE_DEFAULT,
(self._services[domain][service],
service_call))
self._check_busy()
except KeyError: # if key domain or service does not exist
raise ServiceDoesNotExistError(
u"Service does not exist: {}/{}".format(domain, service))
def register_service(self, domain, service, service_func):
""" Register a service. """
with self.service_lock:
try:
self._services[domain][service] = service_func
except KeyError: # Domain does not exist yet in self._services
self._services[domain] = {service: service_func}
def fire_event(self, event_type, event_data=None):
""" Fire an event. """
with self.event_lock:
# Copy the list of the current listeners because some listeners
# remove themselves as a listener while being executed which
# causes the iterator to be confused.
get = self._event_listeners.get
listeners = get(MATCH_ALL, []) + get(event_type, [])
event = Event(event_type, event_data)
self.logger.info(u"Bus:Handling {}".format(event))
if not listeners:
return
if event_type == EVENT_TIME_CHANGED:
prio = PRIO_EVENT_TIME
elif event_type == EVENT_STATE_CHANGED:
prio = PRIO_EVENT_STATE
else:
prio = PRIO_EVENT_DEFAULT
for func in listeners:
self.pool.add_job(prio, (func, event))
self._check_busy()
def listen_event(self, event_type, listener):
""" Listen for all events or events of a specific type.
To listen to all events specify the constant ``MATCH_ALL``
as event_type.
"""
with self.event_lock:
try:
self._event_listeners[event_type].append(listener)
except KeyError: # event_type did not exist
self._event_listeners[event_type] = [listener]
def listen_once_event(self, event_type, listener):
""" Listen once for event of a specific type.
To listen to all events specify the constant ``MATCH_ALL``
as event_type.
Note: at the moment it is impossible to remove a one time listener.
"""
@ft.wraps(listener)
def onetime_listener(event):
""" Removes listener from eventbus and then fires listener. """
if not hasattr(onetime_listener, 'run'):
# Set variable so that we will never run twice.
# Because the event bus might have to wait till a thread comes
# available to execute this listener it might occur that the
# listener gets lined up twice to be executed.
# This will make sure the second time it does nothing.
onetime_listener.run = True
self.remove_event_listener(event_type, onetime_listener)
listener(event)
self.listen_event(event_type, onetime_listener)
def remove_event_listener(self, event_type, listener):
""" Removes a listener of a specific event_type. """
with self.event_lock:
try:
self._event_listeners[event_type].remove(listener)
# delete event_type list if empty
if not self._event_listeners[event_type]:
self._event_listeners.pop(event_type)
except (KeyError, AttributeError):
# KeyError is key event_type listener did not exist
# AttributeError if listener did not exist within event_type
pass
def _check_busy(self):
""" Complain if we have more than twice as many jobs queued as threads
and if we didn't complain about it recently. """
if self.pool.queue.qsize() / self.thread_count >= 2 and \
dt.datetime.now()-self.last_busy_notice > BUS_REPORT_BUSY_TIMEOUT:
self.last_busy_notice = dt.datetime.now()
log_error = self.logger.error
log_error(
u"Bus:All {} threads are busy and {} jobs pending".format(
self.thread_count, self.pool.queue.qsize()))
jobs = self.pool.current_jobs
for start, job in jobs:
log_error(u"Bus:Current job from {}: {}".format(
util.datetime_to_str(start), job))
class State(object):
""" Object to represent a state within the state machine. """
__slots__ = ['entity_id', 'state', 'attributes', 'last_changed']
def __init__(self, entity_id, state, attributes=None, last_changed=None):
self.entity_id = entity_id
self.state = state
self.attributes = attributes or {}
last_changed = last_changed or dt.datetime.now()
# Strip microsecond from last_changed else we cannot guarantee
# state == State.from_dict(state.as_dict())
# This behavior occurs because to_dict uses datetime_to_str
# which strips microseconds
if last_changed.microsecond:
self.last_changed = last_changed - dt.timedelta(
microseconds=last_changed.microsecond)
else:
self.last_changed = last_changed
def copy(self):
""" Creates a copy of itself. """
return State(self.entity_id, self.state,
dict(self.attributes), self.last_changed)
def as_dict(self):
""" Converts State to a dict to be used within JSON.
Ensures: state == State.from_dict(state.as_dict()) """
return {'entity_id': self.entity_id,
'state': self.state,
'attributes': self.attributes,
'last_changed': util.datetime_to_str(self.last_changed)}
@staticmethod
def from_dict(json_dict):
""" Static method to create a state from a dict.
Ensures: state == State.from_json_dict(state.to_json_dict()) """
try:
last_changed = json_dict.get('last_changed')
if last_changed:
last_changed = util.str_to_datetime(last_changed)
return State(json_dict['entity_id'],
json_dict['state'],
json_dict.get('attributes'),
last_changed)
except KeyError: # if key 'entity_id' or 'state' did not exist
return None
def __repr__(self):
if self.attributes:
return u"<state {}:{} @ {}>".format(
self.state, util.repr_helper(self.attributes),
util.datetime_to_str(self.last_changed))
else:
return u"<state {} @ {}>".format(
self.state, util.datetime_to_str(self.last_changed))
class StateMachine(object):
""" Helper class that tracks the state of different entities. """
def __init__(self, bus):
self.states = {}
self.bus = bus
self.lock = threading.Lock()
@property
def entity_ids(self):
""" List of entitie ids that are being tracked. """
with self.lock:
return self.states.keys()
def remove_entity(self, entity_id):
""" Removes a entity from the state machine.
Returns boolean to indicate if a entity was removed. """
with self.lock:
try:
del self.states[entity_id]
return True
except KeyError:
# if entity does not exist
return False
def set_state(self, entity_id, new_state, attributes=None):
""" Set the state of an entity, add entity if it does not exist.
Attributes is an optional dict to specify attributes of this state. """
attributes = attributes or {}
with self.lock:
# Change state and fire listeners
try:
old_state = self.states[entity_id]
except KeyError:
# If state did not exist yet
self.states[entity_id] = State(entity_id, new_state,
attributes)
else:
if old_state.state != new_state or \
old_state.attributes != attributes:
state = self.states[entity_id] = \
State(entity_id, new_state, attributes)
self.bus.fire_event(EVENT_STATE_CHANGED,
{'entity_id': entity_id,
'old_state': old_state,
'new_state': state})
def get_state(self, entity_id):
""" Returns the state of the specified entity. """
with self.lock:
try:
# Make a copy so people won't mutate the state
return self.states[entity_id].copy()
except KeyError:
# If entity does not exist
return None
def is_state(self, entity_id, state):
""" Returns True if entity exists and is specified state. """
try:
return self.get_state(entity_id).state == state
except AttributeError:
# get_state returned None
return False
class Timer(threading.Thread):
""" Timer will sent out an event every TIMER_INTERVAL seconds. """
def __init__(self, bus):
threading.Thread.__init__(self)
self.daemon = True
self.bus = bus
bus.listen_once_event(EVENT_HOMEASSISTANT_START,
lambda event: self.start())
def run(self):
""" Start the timer. """
logging.getLogger(__name__).info("Timer:starting")
last_fired_on_second = -1
calc_now = dt.datetime.now
while True:
now = calc_now()
# First check checks if we are not on a second matching the
# timer interval. Second check checks if we did not already fire
# this interval.
if now.second % TIMER_INTERVAL or \
now.second == last_fired_on_second:
# Sleep till it is the next time that we have to fire an event.
# Aim for halfway through the second that fits TIMER_INTERVAL.
# If TIMER_INTERVAL is 10 fire at .5, 10.5, 20.5, etc seconds.
# This will yield the best results because time.sleep() is not
# 100% accurate because of non-realtime OS's
slp_seconds = TIMER_INTERVAL - now.second % TIMER_INTERVAL + \
.5 - now.microsecond/1000000.0
time.sleep(slp_seconds)
now = calc_now()
last_fired_on_second = now.second
self.bus.fire_event(EVENT_TIME_CHANGED,
{'now': now})
class HomeAssistantError(Exception):
""" General Home Assistant exception occured. """
class ServiceDoesNotExistError(HomeAssistantError):
""" A service has been referenced that deos not exist. """

103
homeassistant/__main__.py Normal file
View File

@@ -0,0 +1,103 @@
""" Starts home assistant. """
from __future__ import print_function
import sys
import os
import argparse
from homeassistant import bootstrap
import homeassistant.config as config_util
from homeassistant.const import EVENT_HOMEASSISTANT_START
def ensure_config_path(config_dir):
""" Validates configuration directory. """
lib_dir = os.path.join(config_dir, 'lib')
# Test if configuration directory exists
if not os.path.isdir(config_dir):
if config_dir != config_util.get_default_config_dir():
print(('Fatal Error: Specified configuration directory does '
'not exist {} ').format(config_dir))
sys.exit(1)
try:
os.mkdir(config_dir)
except OSError:
print(('Fatal Error: Unable to create default configuration '
'directory {} ').format(config_dir))
sys.exit(1)
# Test if library directory exists
if not os.path.isdir(lib_dir):
try:
os.mkdir(lib_dir)
except OSError:
print(('Fatal Error: Unable to create library '
'directory {} ').format(lib_dir))
sys.exit(1)
def ensure_config_file(config_dir):
""" Ensure configuration file exists. """
config_path = config_util.ensure_config_exists(config_dir)
if config_path is None:
print('Error getting configuration path')
sys.exit(1)
return config_path
def get_arguments():
""" Get parsed passed in arguments. """
parser = argparse.ArgumentParser(
description="Home Assistant: Observe, Control, Automate.")
parser.add_argument(
'-c', '--config',
metavar='path_to_config_dir',
default=config_util.get_default_config_dir(),
help="Directory that contains the Home Assistant configuration")
parser.add_argument(
'--demo-mode',
action='store_true',
help='Start Home Assistant in demo mode')
parser.add_argument(
'--open-ui',
action='store_true',
help='Open the webinterface in a browser')
return parser.parse_args()
def main():
""" Starts Home Assistant. """
args = get_arguments()
config_dir = os.path.join(os.getcwd(), args.config)
ensure_config_path(config_dir)
if args.demo_mode:
hass = bootstrap.from_config_dict({
'frontend': {},
'demo': {}
}, config_dir=config_dir)
else:
config_file = ensure_config_file(config_dir)
hass = bootstrap.from_config_file(config_file)
if args.open_ui:
def open_browser(event):
""" Open the webinterface in a browser. """
if hass.config.api is not None:
import webbrowser
webbrowser.open(hass.config.api.base_url)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
hass.start()
hass.block_till_stopped()
if __name__ == "__main__":
main()

View File

@@ -1,199 +1,340 @@
"""
homeassistant.bootstrap
~~~~~~~~~~~~~~~~~~~~~~~
Provides methods to bootstrap a home assistant instance.
Each method will return a tuple (bus, statemachine).
After bootstrapping you can add your own components or
start by calling homeassistant.start_home_assistant(bus)
"""
import importlib
import ConfigParser
import os
import sys
import logging
from collections import defaultdict
import homeassistant as ha
import homeassistant.components as components
import homeassistant.core as core
import homeassistant.util.dt as date_util
import homeassistant.util.package as pkg_util
import homeassistant.util.location as loc_util
import homeassistant.config as config_util
import homeassistant.loader as loader
import homeassistant.components as core_components
import homeassistant.components.group as group
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE,
CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE, CONF_CUSTOMIZE,
TEMP_CELCIUS, TEMP_FAHRENHEIT)
_LOGGER = logging.getLogger(__name__)
ATTR_COMPONENT = 'component'
PLATFORM_FORMAT = '{}.{}'
# pylint: disable=too-many-branches,too-many-locals,too-many-statements
def from_config_file(config_path):
""" Starts home assistant with all possible functionality
based on a config file. """
def setup_component(hass, domain, config=None):
""" Setup a component and all its dependencies. """
# Setup the logging for home assistant.
logging.basicConfig(level=logging.INFO)
if domain in hass.config.components:
return True
# Log errors to a file
err_handler = logging.FileHandler("home-assistant.log",
mode='w', delay=True)
err_handler.setLevel(logging.ERROR)
err_handler.setFormatter(
logging.Formatter('%(asctime)s %(name)s: %(message)s',
datefmt='%H:%M %d-%m-%y'))
logging.getLogger('').addHandler(err_handler)
_ensure_loader_prepared(hass)
# Start the actual bootstrapping
logger = logging.getLogger(__name__)
if config is None:
config = defaultdict(dict)
statusses = []
components = loader.load_order_component(domain)
# Read config
config = ConfigParser.SafeConfigParser()
config.read(config_path)
# OrderedSet is empty if component or dependencies could not be resolved
if not components:
return False
# Init core
bus = ha.Bus()
statemachine = ha.StateMachine(bus)
for component in components:
if component in hass.config.components:
continue
has_opt = config.has_option
get_opt = config.get
has_section = config.has_section
add_status = lambda name, result: statusses.append((name, result))
load_module = lambda module: importlib.import_module(
'homeassistant.components.'+module)
if not _setup_component(hass, component, config):
return False
def get_opt_safe(section, option, default=None):
""" Failure proof option retriever. """
try:
return config.get(section, option)
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
return default
return True
# Device scanner
dev_scan = None
def _handle_requirements(hass, component, name):
""" Installs requirements for component. """
if not hasattr(component, 'REQUIREMENTS'):
return True
for req in component.REQUIREMENTS:
if not pkg_util.install_package(req, target=hass.config.path('lib')):
_LOGGER.error('Not initializing %s because could not install '
'dependency %s', name, req)
return False
return True
def _setup_component(hass, domain, config):
""" Setup a component for Home Assistant. """
component = loader.get_component(domain)
missing_deps = [dep for dep in component.DEPENDENCIES
if dep not in hass.config.components]
if missing_deps:
_LOGGER.error(
'Not initializing %s because not all dependencies loaded: %s',
domain, ", ".join(missing_deps))
return False
if not _handle_requirements(hass, component, domain):
return False
try:
# For the error message if not all option fields exist
opt_fields = "host, username, password"
if not component.setup(hass, config):
_LOGGER.error('component %s failed to initialize', domain)
return False
except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error during setup of component %s', domain)
return False
if has_section('device_tracker.tomato'):
device_tracker = load_module('device_tracker')
hass.config.components.append(component.DOMAIN)
dev_scan_name = "Tomato"
opt_fields += ", http_id"
# Assumption: if a component does not depend on groups
# it communicates with devices
if group.DOMAIN not in component.DEPENDENCIES:
hass.pool.add_worker()
dev_scan = device_tracker.TomatoDeviceScanner(
get_opt('device_tracker.tomato', 'host'),
get_opt('device_tracker.tomato', 'username'),
get_opt('device_tracker.tomato', 'password'),
get_opt('device_tracker.tomato', 'http_id'))
hass.bus.fire(
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN})
elif has_section('device_tracker.netgear'):
device_tracker = load_module('device_tracker')
return True
dev_scan_name = "Netgear"
dev_scan = device_tracker.NetgearDeviceScanner(
get_opt('device_tracker.netgear', 'host'),
get_opt('device_tracker.netgear', 'username'),
get_opt('device_tracker.netgear', 'password'))
def prepare_setup_platform(hass, config, domain, platform_name):
""" Loads a platform and makes sure dependencies are setup. """
_ensure_loader_prepared(hass)
except ConfigParser.NoOptionError:
# If one of the options didn't exist
logger.exception(("Error initializing {}DeviceScanner, "
"could not find one of the following config "
"options: {}".format(dev_scan_name, opt_fields)))
platform_path = PLATFORM_FORMAT.format(domain, platform_name)
add_status("Device Scanner - {}".format(dev_scan_name), False)
platform = loader.get_component(platform_path)
if dev_scan:
add_status("Device Scanner - {}".format(dev_scan_name),
dev_scan.success_init)
# Not found
if platform is None:
return None
if not dev_scan.success_init:
dev_scan = None
# Already loaded
elif platform_path in hass.config.components:
return platform
# Device Tracker
if dev_scan:
device_tracker.DeviceTracker(bus, statemachine, dev_scan)
# Load dependencies
if hasattr(platform, 'DEPENDENCIES'):
for component in platform.DEPENDENCIES:
if not setup_component(hass, component, config):
_LOGGER.error(
'Unable to prepare setup for platform %s because '
'dependency %s could not be initialized', platform_path,
component)
return None
add_status("Device Tracker", True)
if not _handle_requirements(hass, platform, platform_path):
return None
# Sun tracker
if has_opt("common", "latitude") and \
has_opt("common", "longitude"):
return platform
sun = load_module('sun')
add_status("Weather - Ephem",
sun.setup(
bus, statemachine,
get_opt("common", "latitude"),
get_opt("common", "longitude")))
def mount_local_lib_path(config_dir):
""" Add local library to Python Path """
sys.path.insert(0, os.path.join(config_dir, 'lib'))
# pylint: disable=too-many-branches, too-many-statements
def from_config_dict(config, hass=None, config_dir=None, enable_log=True):
"""
Tries to configure Home Assistant from a config dict.
Dynamically loads required components and its dependencies.
"""
if hass is None:
hass = core.HomeAssistant()
if config_dir is not None:
config_dir = os.path.abspath(config_dir)
hass.config.config_dir = config_dir
mount_local_lib_path(config_dir)
process_ha_core_config(hass, config.get(core.DOMAIN, {}))
if enable_log:
enable_logging(hass)
_ensure_loader_prepared(hass)
# Make a copy because we are mutating it.
# Convert it to defaultdict so components can always have config dict
# Convert values to dictionaries if they are None
config = defaultdict(
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)
if not core_components.setup(hass, config):
_LOGGER.error('Home Assistant core failed to initialize. '
'Further initialization aborted.')
return hass
_LOGGER.info('Home Assistant core initialized')
# Setup the components
for domain in loader.load_order_components(components):
_setup_component(hass, domain, config)
return hass
def from_config_file(config_path, hass=None):
"""
Reads the configuration file and tries to start all the required
functionality. Will add functionality to 'hass' parameter if given,
instantiates a new Home Assistant object if 'hass' is not given.
"""
if hass is None:
hass = core.HomeAssistant()
# Set config dir to directory holding config file
config_dir = os.path.abspath(os.path.dirname(config_path))
hass.config.config_dir = config_dir
mount_local_lib_path(config_dir)
enable_logging(hass)
config_dict = config_util.load_config_file(config_path)
return from_config_dict(config_dict, hass, enable_log=False)
def enable_logging(hass):
""" Setup the logging for home assistant. """
logging.basicConfig(level=logging.INFO)
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
"[%(name)s] %(message)s%(reset)s")
try:
from colorlog import ColoredFormatter
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
fmt,
datefmt='%y-%m-%d %H:%M:%S',
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
))
except ImportError:
_LOGGER.warning(
"Colorlog package not found, console coloring disabled")
# Log errors to a file if we have write access to file or config dir
err_log_path = hass.config.path('home-assistant.log')
err_path_exists = os.path.isfile(err_log_path)
# Check if we can write to the error log if it exists or that
# we can create files in the containing directory if not.
if (err_path_exists and os.access(err_log_path, os.W_OK)) or \
(not err_path_exists and os.access(hass.config.config_dir, os.W_OK)):
err_handler = logging.FileHandler(
err_log_path, mode='w', delay=True)
err_handler.setLevel(logging.WARNING)
err_handler.setFormatter(
logging.Formatter('%(asctime)s %(name)s: %(message)s',
datefmt='%y-%m-%d %H:%M:%S'))
logging.getLogger('').addHandler(err_handler)
else:
sun = None
_LOGGER.error(
'Unable to setup error log %s (access denied)', err_log_path)
# Chromecast
if has_section("chromecast"):
chromecast = load_module('chromecast')
chromecast_started = chromecast.setup(bus, statemachine)
def process_ha_core_config(hass, config):
""" Processes the [homeassistant] section from the config. """
hac = hass.config
add_status("Chromecast", chromecast_started)
else:
chromecast_started = False
def set_time_zone(time_zone_str):
""" Helper method to set time zone in HA. """
if time_zone_str is None:
return
# WeMo
if has_section("wemo"):
wemo = load_module('wemo')
time_zone = date_util.get_time_zone(time_zone_str)
add_status("WeMo", wemo.setup(bus, statemachine))
if time_zone:
hac.time_zone = time_zone
date_util.set_default_time_zone(time_zone)
else:
_LOGGER.error('Received invalid time zone %s', time_zone_str)
# Light control
if has_section("light.hue"):
light = load_module('light')
for key, attr in ((CONF_LATITUDE, 'latitude'),
(CONF_LONGITUDE, 'longitude'),
(CONF_NAME, 'location_name')):
if key in config:
setattr(hac, attr, config[key])
light_control = light.HueLightControl(get_opt_safe("hue", "host"))
set_time_zone(config.get(CONF_TIME_ZONE))
add_status("Light - Hue", light_control.success_init)
customize = config.get(CONF_CUSTOMIZE)
light.setup(bus, statemachine, light_control)
else:
light_control = None
if isinstance(customize, dict):
for entity_id, attrs in config.get(CONF_CUSTOMIZE, {}).items():
if not isinstance(attrs, dict):
continue
Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values())
if has_opt("downloader", "download_dir"):
downloader = load_module('downloader')
if CONF_TEMPERATURE_UNIT in config:
unit = config[CONF_TEMPERATURE_UNIT]
add_status("Downloader", downloader.setup(
bus, get_opt("downloader", "download_dir")))
if unit == 'C':
hac.temperature_unit = TEMP_CELCIUS
elif unit == 'F':
hac.temperature_unit = TEMP_FAHRENHEIT
add_status("Core components", components.setup(bus, statemachine))
# If we miss some of the needed values, auto detect them
if None not in (
hac.latitude, hac.longitude, hac.temperature_unit, hac.time_zone):
return
if has_section('browser'):
add_status("Browser", load_module('browser').setup(bus))
_LOGGER.info('Auto detecting location and temperature unit')
if has_section('keyboard'):
add_status("Keyboard", load_module('keyboard').setup(bus))
info = loc_util.detect_location_info()
# Init HTTP interface
if has_opt("httpinterface", "api_password"):
httpinterface = load_module('httpinterface')
if info is None:
_LOGGER.error('Could not detect location information')
return
httpinterface.HTTPInterface(
bus, statemachine,
get_opt("httpinterface", "api_password"))
if hac.latitude is None and hac.longitude is None:
hac.latitude = info.latitude
hac.longitude = info.longitude
add_status("HTTPInterface", True)
if hac.temperature_unit is None:
if info.use_fahrenheit:
hac.temperature_unit = TEMP_FAHRENHEIT
else:
hac.temperature_unit = TEMP_CELCIUS
# Init groups
if has_section("group"):
group = load_module('group')
if hac.location_name is None:
hac.location_name = info.city
for name, entity_ids in config.items("group"):
add_status("Group - {}".format(name),
group.setup(bus, statemachine, name,
entity_ids.split(",")))
if hac.time_zone is None:
set_time_zone(info.time_zone)
# Light trigger
if light_control and sun:
device_sun_light_trigger = load_module('device_sun_light_trigger')
light_group = get_opt_safe("device_sun_light_trigger", "light_group")
light_profile = get_opt_safe("device_sun_light_trigger",
"light_profile")
add_status("Device Sun Light Trigger",
device_sun_light_trigger.setup(bus, statemachine,
light_group, light_profile))
for component, success_init in statusses:
status = "initialized" if success_init else "Failed to initialize"
logger.info("{}: {}".format(component, status))
ha.start_home_assistant(bus)
def _ensure_loader_prepared(hass):
""" Ensure Home Assistant loader is prepared. """
if not loader.PREPARED:
loader.prepare(hass)

View File

@@ -15,128 +15,73 @@ Each component should publish services only under its own domain.
"""
import itertools as it
import importlib
import logging
import homeassistant as ha
import homeassistant.core as ha
import homeassistant.util as util
from homeassistant.helpers import extract_entity_ids
from homeassistant.loader import get_component
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
# Contains one string or a list of strings, each being an entity id
ATTR_ENTITY_ID = 'entity_id'
# String with a friendly name for the entity
ATTR_FRIENDLY_NAME = "friendly_name"
STATE_ON = 'on'
STATE_OFF = 'off'
STATE_NOT_HOME = 'not_home'
STATE_HOME = 'home'
SERVICE_TURN_ON = 'turn_on'
SERVICE_TURN_OFF = 'turn_off'
SERVICE_VOLUME_UP = "volume_up"
SERVICE_VOLUME_DOWN = "volume_down"
SERVICE_VOLUME_MUTE = "volume_mute"
SERVICE_MEDIA_PLAY_PAUSE = "media_play_pause"
SERVICE_MEDIA_NEXT_TRACK = "media_next_track"
SERVICE_MEDIA_PREV_TRACK = "media_prev_track"
_LOADED_COMP = {}
_LOGGER = logging.getLogger(__name__)
def _get_component(component):
""" Returns requested component. Imports it if necessary. """
comps = _LOADED_COMP
# See if we have the module locally cached, else import it
try:
return comps[component]
except KeyError:
# If comps[component] does not exist, import module
try:
comps[component] = importlib.import_module(
'homeassistant.components.'+component)
except ImportError:
# If we got a bogus component the input will fail
comps[component] = None
return comps[component]
def is_on(statemachine, entity_id=None):
def is_on(hass, entity_id=None):
""" Loads up the module to call the is_on method.
If there is no entity id given we will check all. """
if entity_id:
group = _get_component('group')
group = get_component('group')
entity_ids = group.expand_entity_ids([entity_id])
entity_ids = group.expand_entity_ids(hass, [entity_id])
else:
entity_ids = statemachine.entity_ids
entity_ids = hass.states.entity_ids()
for entity_id in entity_ids:
domain = util.split_entity_id(entity_id)[0]
module = _get_component(domain)
module = get_component(domain)
try:
if module.is_on(statemachine, entity_id):
if module.is_on(hass, entity_id):
return True
except AttributeError:
# module is None or method is_on does not exist
pass
_LOGGER.exception("Failed to call %s.is_on for %s",
module, entity_id)
return False
def turn_on(bus, **service_data):
def turn_on(hass, entity_id=None, **service_data):
""" Turns specified entity on if possible. """
bus.call_service(ha.DOMAIN, SERVICE_TURN_ON, service_data)
if entity_id is not None:
service_data[ATTR_ENTITY_ID] = entity_id
hass.services.call(ha.DOMAIN, SERVICE_TURN_ON, service_data)
def turn_off(bus, **service_data):
def turn_off(hass, entity_id=None, **service_data):
""" Turns specified entity off. """
bus.call_service(ha.DOMAIN, SERVICE_TURN_OFF, service_data)
if entity_id is not None:
service_data[ATTR_ENTITY_ID] = entity_id
hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data)
def extract_entity_ids(statemachine, service):
"""
Helper method to extract a list of entity ids from a service call.
Will convert group entity ids to the entity ids it represents.
"""
entity_ids = []
if service.data and ATTR_ENTITY_ID in service.data:
group = _get_component('group')
# Entity ID attr can be a list or a string
service_ent_id = service.data[ATTR_ENTITY_ID]
if isinstance(service_ent_id, list):
ent_ids = service_ent_id
else:
ent_ids = [service_ent_id]
entity_ids.extend(
ent_id for ent_id
in group.expand_entity_ids(statemachine, ent_ids)
if ent_id not in entity_ids)
return entity_ids
def setup(bus, statemachine):
def setup(hass, config):
""" Setup general services related to homeassistant. """
def handle_turn_service(service):
""" Method to handle calls to homeassistant.turn_on/off. """
entity_ids = extract_entity_ids(statemachine, service)
entity_ids = extract_entity_ids(hass, service)
# Generic turn on/off method requires entity id
if not entity_ids:
_LOGGER.error(
"homeassistant/%s cannot be called without entity_id",
service.service)
return
# Group entity_ids by domain. groupby requires sorted data.
@@ -150,13 +95,9 @@ def setup(bus, statemachine):
# ent_ids is a generator, convert it to a list.
data[ATTR_ENTITY_ID] = list(ent_ids)
try:
bus.call_service(domain, service.service, data)
except ha.ServiceDoesNotExistError:
# turn_on service does not exist
pass
hass.services.call(domain, service.service, data, True)
bus.register_service(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
bus.register_service(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
return True

View File

@@ -0,0 +1,346 @@
"""
homeassistant.components.api
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides a Rest API for Home Assistant.
"""
import re
import logging
import threading
import json
import homeassistant.core as ha
from homeassistant.helpers.state import TrackStates
import homeassistant.remote as rem
from homeassistant.const import (
URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM,
URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS,
URL_API_CONFIG, URL_API_BOOTSTRAP,
EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL,
HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
HTTP_UNPROCESSABLE_ENTITY)
DOMAIN = 'api'
DEPENDENCIES = ['http']
STREAM_PING_PAYLOAD = "ping"
STREAM_PING_INTERVAL = 50 # seconds
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
""" Register the API with the HTTP interface. """
if 'http' not in hass.config.components:
_LOGGER.error('Dependency http is not loaded')
return False
# /api - for validation purposes
hass.http.register_path('GET', URL_API, _handle_get_api)
# /api/stream
hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream)
# /api/config
hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config)
# /api/bootstrap
hass.http.register_path(
'GET', URL_API_BOOTSTRAP, _handle_get_api_bootstrap)
# /states
hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states)
hass.http.register_path(
'GET', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_handle_get_api_states_entity)
hass.http.register_path(
'POST', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_handle_post_state_entity)
hass.http.register_path(
'PUT', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_handle_post_state_entity)
# /events
hass.http.register_path('GET', URL_API_EVENTS, _handle_get_api_events)
hass.http.register_path(
'POST', re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
_handle_api_post_events_event)
# /services
hass.http.register_path('GET', URL_API_SERVICES, _handle_get_api_services)
hass.http.register_path(
'POST',
re.compile((r'/api/services/'
r'(?P<domain>[a-zA-Z\._0-9]+)/'
r'(?P<service>[a-zA-Z\._0-9]+)')),
_handle_post_api_services_domain_service)
# /event_forwarding
hass.http.register_path(
'POST', URL_API_EVENT_FORWARD, _handle_post_api_event_forward)
hass.http.register_path(
'DELETE', URL_API_EVENT_FORWARD, _handle_delete_api_event_forward)
# /components
hass.http.register_path(
'GET', URL_API_COMPONENTS, _handle_get_api_components)
return True
def _handle_get_api(handler, path_match, data):
""" Renders the debug interface. """
handler.write_json_message("API running.")
def _handle_get_api_stream(handler, path_match, data):
""" Provide a streaming interface for the event bus. """
gracefully_closed = False
hass = handler.server.hass
wfile = handler.wfile
write_lock = threading.Lock()
block = threading.Event()
def write_message(payload):
""" Writes a message to the output. """
with write_lock:
msg = "data: {}\n\n".format(payload)
try:
wfile.write(msg.encode("UTF-8"))
wfile.flush()
except IOError:
block.set()
def forward_events(event):
""" Forwards events to the open request. """
nonlocal gracefully_closed
if block.is_set() or event.event_type == EVENT_TIME_CHANGED:
return
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
gracefully_closed = True
block.set()
return
write_message(json.dumps(event, cls=rem.JSONEncoder))
handler.send_response(HTTP_OK)
handler.send_header('Content-type', 'text/event-stream')
handler.end_headers()
hass.bus.listen(MATCH_ALL, forward_events)
while True:
write_message(STREAM_PING_PAYLOAD)
block.wait(STREAM_PING_INTERVAL)
if block.is_set():
break
if not gracefully_closed:
_LOGGER.info("Found broken event stream to %s, cleaning up",
handler.client_address[0])
hass.bus.remove_listener(MATCH_ALL, forward_events)
def _handle_get_api_config(handler, path_match, data):
""" Returns the Home Assistant config. """
handler.write_json(handler.server.hass.config.as_dict())
def _handle_get_api_bootstrap(handler, path_match, data):
""" Returns all data needed to bootstrap Home Assistant. """
hass = handler.server.hass
handler.write_json({
'config': hass.config.as_dict(),
'states': hass.states.all(),
'events': _events_json(hass),
'services': _services_json(hass),
})
def _handle_get_api_states(handler, path_match, data):
""" Returns a dict containing all entity ids and their state. """
handler.write_json(handler.server.hass.states.all())
def _handle_get_api_states_entity(handler, path_match, data):
""" Returns the state of a specific entity. """
entity_id = path_match.group('entity_id')
state = handler.server.hass.states.get(entity_id)
if state:
handler.write_json(state)
else:
handler.write_json_message("State does not exist.", HTTP_NOT_FOUND)
def _handle_post_state_entity(handler, path_match, data):
""" Handles updating the state of an entity.
This handles the following paths:
/api/states/<entity_id>
"""
entity_id = path_match.group('entity_id')
try:
new_state = data['state']
except KeyError:
handler.write_json_message("state not specified", HTTP_BAD_REQUEST)
return
attributes = data['attributes'] if 'attributes' in data else None
is_new_state = handler.server.hass.states.get(entity_id) is None
# Write state
handler.server.hass.states.set(entity_id, new_state, attributes)
state = handler.server.hass.states.get(entity_id)
status_code = HTTP_CREATED if is_new_state else HTTP_OK
handler.write_json(
state.as_dict(),
status_code=status_code,
location=URL_API_STATES_ENTITY.format(entity_id))
def _handle_get_api_events(handler, path_match, data):
""" Handles getting overview of event listeners. """
handler.write_json(_events_json(handler.server.hass))
def _handle_api_post_events_event(handler, path_match, event_data):
""" Handles firing of an event.
This handles the following paths:
/api/events/<event_type>
Events from /api are threated as remote events.
"""
event_type = path_match.group('event_type')
if event_data is not None and not isinstance(event_data, dict):
handler.write_json_message(
"event_data should be an object", HTTP_UNPROCESSABLE_ENTITY)
event_origin = ha.EventOrigin.remote
# Special case handling for event STATE_CHANGED
# We will try to convert state dicts back to State objects
if event_type == ha.EVENT_STATE_CHANGED and event_data:
for key in ('old_state', 'new_state'):
state = ha.State.from_dict(event_data.get(key))
if state:
event_data[key] = state
handler.server.hass.bus.fire(event_type, event_data, event_origin)
handler.write_json_message("Event {} fired.".format(event_type))
def _handle_get_api_services(handler, path_match, data):
""" Handles getting overview of services. """
handler.write_json(_services_json(handler.server.hass))
# pylint: disable=invalid-name
def _handle_post_api_services_domain_service(handler, path_match, data):
""" Handles calling a service.
This handles the following paths:
/api/services/<domain>/<service>
"""
domain = path_match.group('domain')
service = path_match.group('service')
with TrackStates(handler.server.hass) as changed_states:
handler.server.hass.services.call(domain, service, data, True)
handler.write_json(changed_states)
# pylint: disable=invalid-name
def _handle_post_api_event_forward(handler, path_match, data):
""" Handles adding an event forwarding target. """
try:
host = data['host']
api_password = data['api_password']
except KeyError:
handler.write_json_message(
"No host or api_password received.", HTTP_BAD_REQUEST)
return
try:
port = int(data['port']) if 'port' in data else None
except ValueError:
handler.write_json_message(
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
return
api = rem.API(host, api_password, port)
if not api.validate_api():
handler.write_json_message(
"Unable to validate API", HTTP_UNPROCESSABLE_ENTITY)
return
if handler.server.event_forwarder is None:
handler.server.event_forwarder = \
rem.EventForwarder(handler.server.hass)
handler.server.event_forwarder.connect(api)
handler.write_json_message("Event forwarding setup.")
def _handle_delete_api_event_forward(handler, path_match, data):
""" Handles deleting an event forwarding target. """
try:
host = data['host']
except KeyError:
handler.write_json_message("No host received.", HTTP_BAD_REQUEST)
return
try:
port = int(data['port']) if 'port' in data else None
except ValueError:
handler.write_json_message(
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
return
if handler.server.event_forwarder is not None:
api = rem.API(host, None, port)
handler.server.event_forwarder.disconnect(api)
handler.write_json_message("Event forwarding cancelled.")
def _handle_get_api_components(handler, path_match, data):
""" Returns all the loaded components. """
handler.write_json(handler.server.hass.config.components)
def _services_json(hass):
""" Generate services data to JSONify. """
return [{"domain": key, "services": value}
for key, value in hass.services.services.items()]
def _events_json(hass):
""" Generate event data to JSONify. """
return [{"event": key, "listener_count": value}
for key, value in hass.bus.listeners.items()]

View File

@@ -0,0 +1,143 @@
"""
components.arduino
~~~~~~~~~~~~~~~~~~
Arduino component that connects to a directly attached Arduino board which
runs with the Firmata firmware.
Configuration:
To use the Arduino board you will need to add something like the following
to your config/configuration.yaml
arduino:
port: /dev/ttyACM0
Variables:
port
*Required
The port where is your board connected to your Home Assistant system.
If you are using an original Arduino the port will be named ttyACM*. The exact
number can be determined with 'ls /dev/ttyACM*' or check your 'dmesg'/
'journalctl -f' output. Keep in mind that Arduino clones are often using a
different name for the port (e.g. '/dev/ttyUSB*').
A word of caution: The Arduino is not storing states. This means that with
every initialization the pins are set to off/low.
"""
import logging
try:
from PyMata.pymata import PyMata
except ImportError:
PyMata = None
from homeassistant.helpers import validate_config
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP)
DOMAIN = "arduino"
DEPENDENCIES = []
REQUIREMENTS = ['PyMata==2.07a']
BOARD = None
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
""" Setup the Arduino component. """
global PyMata # pylint: disable=invalid-name
if PyMata is None:
from PyMata.pymata import PyMata as PyMata_
PyMata = PyMata_
import serial
if not validate_config(config,
{DOMAIN: ['port']},
_LOGGER):
return False
global BOARD
try:
BOARD = ArduinoBoard(config[DOMAIN]['port'])
except (serial.serialutil.SerialException, FileNotFoundError):
_LOGGER.exception("Your port is not accessible.")
return False
if BOARD.get_firmata()[1] <= 2:
_LOGGER.error("The StandardFirmata sketch should be 2.2 or newer.")
return False
def stop_arduino(event):
""" Stop the Arduino service. """
BOARD.disconnect()
def start_arduino(event):
""" Start the Arduino service. """
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_arduino)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_arduino)
return True
class ArduinoBoard(object):
""" Represents an Arduino board. """
def __init__(self, port):
self._port = port
self._board = PyMata(self._port, verbose=False)
def set_mode(self, pin, direction, mode):
""" Sets the mode and the direction of a given pin. """
if mode == 'analog' and direction == 'in':
self._board.set_pin_mode(pin,
self._board.INPUT,
self._board.ANALOG)
elif mode == 'analog' and direction == 'out':
self._board.set_pin_mode(pin,
self._board.OUTPUT,
self._board.ANALOG)
elif mode == 'digital' and direction == 'in':
self._board.set_pin_mode(pin,
self._board.OUTPUT,
self._board.DIGITAL)
elif mode == 'digital' and direction == 'out':
self._board.set_pin_mode(pin,
self._board.OUTPUT,
self._board.DIGITAL)
elif mode == 'pwm':
self._board.set_pin_mode(pin,
self._board.OUTPUT,
self._board.PWM)
def get_analog_inputs(self):
""" Get the values from the pins. """
self._board.capability_query()
return self._board.get_analog_response_table()
def set_digital_out_high(self, pin):
""" Sets a given digital pin to high. """
self._board.digital_write(pin, 1)
def set_digital_out_low(self, pin):
""" Sets a given digital pin to low. """
self._board.digital_write(pin, 0)
def get_digital_in(self, pin):
""" Gets the value from a given digital pin. """
self._board.digital_read(pin)
def get_analog_in(self, pin):
""" Gets the value from a given analog pin. """
self._board.analog_read(pin)
def get_firmata(self):
""" Return the version of the Firmata firmware. """
return self._board.get_firmata_version()
def disconnect(self):
""" Disconnects the board and closes the serial connection. """
self._board.reset()
self._board.close()

View File

@@ -0,0 +1,74 @@
"""
homeassistant.components.automation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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
DOMAIN = "automation"
DEPENDENCIES = ["group"]
CONF_ALIAS = "alias"
CONF_SERVICE = "execute_service"
CONF_SERVICE_ENTITY_ID = "service_entity_id"
CONF_SERVICE_DATA = "service_data"
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
""" Sets up automation. """
success = False
for p_type, p_config in config_per_platform(config, DOMAIN, _LOGGER):
platform = prepare_setup_platform(hass, config, DOMAIN, p_type)
if platform is None:
_LOGGER.error("Unknown automation platform specified: %s", p_type)
continue
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
else:
_LOGGER.error(
"Error setting up rule %s", p_config.get(CONF_ALIAS, ""))
return success
def _get_action(hass, config):
""" Return an action based on a config. """
def action():
""" Action to be executed. """
_LOGGER.info("Executing rule %s", config.get(CONF_ALIAS, ""))
if CONF_SERVICE in config:
domain, service = split_entity_id(config[CONF_SERVICE])
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 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)
return action

View File

@@ -0,0 +1,31 @@
"""
homeassistant.components.automation.event
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Offers event listening automation rules.
"""
import logging
CONF_EVENT_TYPE = "event_type"
CONF_EVENT_DATA = "event_data"
_LOGGER = logging.getLogger(__name__)
def register(hass, config, action):
""" Listen for events based on config. """
event_type = config.get(CONF_EVENT_TYPE)
if event_type is None:
_LOGGER.error("Missing configuration key %s", CONF_EVENT_TYPE)
return False
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:
action()
hass.bus.listen(event_type, handle_event)
return True

View File

@@ -0,0 +1,34 @@
"""
homeassistant.components.automation.mqtt
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Offers MQTT listening automation rules.
"""
import logging
import homeassistant.components.mqtt as mqtt
DEPENDENCIES = ['mqtt']
CONF_TOPIC = 'mqtt_topic'
CONF_PAYLOAD = 'mqtt_payload'
def register(hass, config, action):
""" Listen for state changes based on `config`. """
topic = config.get(CONF_TOPIC)
payload = config.get(CONF_PAYLOAD)
if topic is None:
logging.getLogger(__name__).error(
"Missing configuration key %s", CONF_TOPIC)
return False
def mqtt_automation_listener(msg_topic, msg_payload, qos):
""" Listens for MQTT messages. """
if payload is None or payload == msg_payload:
action()
mqtt.subscribe(hass, topic, mqtt_automation_listener)
return True

View File

@@ -0,0 +1,37 @@
"""
homeassistant.components.automation.state
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Offers state listening automation rules.
"""
import logging
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"
def register(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)
return False
from_state = config.get(CONF_FROM, MATCH_ALL)
to_state = config.get(CONF_TO, MATCH_ALL)
def state_automation_listener(entity, from_s, to_s):
""" Listens for state changes and calls action. """
action()
track_state_change(
hass, entity_id, state_automation_listener, from_state, to_state)
return True

View File

@@ -0,0 +1,28 @@
"""
homeassistant.components.automation.time
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Offers time listening automation rules.
"""
from homeassistant.util import convert
from homeassistant.helpers.event import track_time_change
CONF_HOURS = "time_hours"
CONF_MINUTES = "time_minutes"
CONF_SECONDS = "time_seconds"
def register(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)
def time_automation_listener(now):
""" Listens for time changes and calls action. """
action()
track_time_change(hass, time_automation_listener,
hour=hours, minute=minutes, second=seconds)
return True

View File

@@ -6,20 +6,21 @@ Provides functionality to launch a webbrowser on the host machine.
"""
DOMAIN = "browser"
DEPENDENCIES = []
SERVICE_BROWSE_URL = "browse_url"
def setup(bus):
def setup(hass, config):
""" Listen for browse_url events and open
the url in the default webbrowser. """
import webbrowser
bus.register_service(DOMAIN, SERVICE_BROWSE_URL,
lambda service:
webbrowser.open(
service.data.get('url',
'https://www.google.com')))
hass.services.register(DOMAIN, SERVICE_BROWSE_URL,
lambda service:
webbrowser.open(
service.data.get(
'url', 'https://www.google.com')))
return True

View File

@@ -0,0 +1,229 @@
# pylint: disable=too-many-lines
"""
homeassistant.components.camera
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Component to interface with various cameras.
The following features are supported:
- Returning recorded camera images and streams
- Proxying image requests via HA for external access
- Converting a still image url into a live video stream
Upcoming features
- Recording
- Snapshot
- Motion Detection Recording(for supported cameras)
- Automatic Configuration(for supported cameras)
- Creation of child entities for supported functions
- Collating motion event images passed via FTP into time based events
- A service for calling camera functions
- Camera movement(panning)
- Zoom
- Light/Nightvision toggling
- Support for more devices
- Expanded documentation
"""
import requests
import logging
import time
import re
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
ATTR_ENTITY_PICTURE,
HTTP_NOT_FOUND,
ATTR_ENTITY_ID,
)
from homeassistant.helpers.entity_component import EntityComponent
DOMAIN = 'camera'
DEPENDENCIES = ['http']
GROUP_NAME_ALL_CAMERAS = 'all_cameras'
SCAN_INTERVAL = 30
ENTITY_ID_FORMAT = DOMAIN + '.{}'
SWITCH_ACTION_RECORD = 'record'
SWITCH_ACTION_SNAPSHOT = 'snapshot'
SERVICE_CAMERA = 'camera_service'
STATE_RECORDING = 'recording'
DEFAULT_RECORDING_SECONDS = 30
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {}
FILE_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S-%f'
DIR_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S'
REC_DIR_PREFIX = 'recording-'
REC_IMG_PREFIX = 'recording_image-'
STATE_STREAMING = 'streaming'
STATE_IDLE = 'idle'
CAMERA_PROXY_URL = '/api/camera_proxy_stream/{0}'
CAMERA_STILL_URL = '/api/camera_proxy/{0}'
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?time={1}'
MULTIPART_BOUNDARY = '--jpegboundary'
MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n'
# pylint: disable=too-many-branches
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)
# -------------------------------------------------------------------------
# CAMERA COMPONENT ENDPOINTS
# -------------------------------------------------------------------------
# The following defines the endpoints for serving images from the camera
# via the HA http server. This is means that you can access images from
# your camera outside of your LAN without the need for port forwards etc.
# Because the authentication header can't be added in image requests these
# endpoints are secured with session based security.
# pylint: disable=unused-argument
def _proxy_camera_image(handler, path_match, data):
""" Proxies the camera image via the HA server. """
entity_id = path_match.group(ATTR_ENTITY_ID)
camera = None
if entity_id in component.entities.keys():
camera = component.entities[entity_id]
if camera:
response = camera.camera_image()
handler.wfile.write(response)
else:
handler.send_response(HTTP_NOT_FOUND)
hass.http.register_path(
'GET',
re.compile(r'/api/camera_proxy/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_proxy_camera_image)
# pylint: disable=unused-argument
def _proxy_camera_mjpeg_stream(handler, path_match, data):
""" Proxies the camera image as an mjpeg stream via the HA server.
This function takes still images from the IP camera and turns them
into an MJPEG stream. This means that HA can return a live video
stream even with only a still image URL available.
"""
entity_id = path_match.group(ATTR_ENTITY_ID)
camera = None
if entity_id in component.entities.keys():
camera = component.entities[entity_id]
if not camera:
handler.send_response(HTTP_NOT_FOUND)
handler.end_headers()
return
try:
camera.is_streaming = True
camera.update_ha_state()
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8'))
handler.request.sendall(bytes(
'Content-type: multipart/x-mixed-replace; \
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8'))
# MJPEG_START_HEADER.format()
while True:
img_bytes = camera.camera_image()
headers_str = '\r\n'.join((
'Content-length: {}'.format(len(img_bytes)),
'Content-type: image/jpeg',
)) + '\r\n\r\n'
handler.request.sendall(
bytes(headers_str, 'utf-8') +
img_bytes +
bytes('\r\n', 'utf-8'))
handler.request.sendall(
bytes('--jpgboundary\r\n', 'utf-8'))
except (requests.RequestException, IOError):
camera.is_streaming = False
camera.update_ha_state()
camera.is_streaming = False
hass.http.register_path(
'GET',
re.compile(
r'/api/camera_proxy_stream/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_proxy_camera_mjpeg_stream)
return True
class Camera(Entity):
""" The base class for camera components """
def __init__(self):
self.is_streaming = False
@property
# pylint: disable=no-self-use
def is_recording(self):
""" Returns true if the device is recording """
return False
@property
# pylint: disable=no-self-use
def brand(self):
""" Should return a string of the camera brand """
return None
@property
# pylint: disable=no-self-use
def model(self):
""" Returns string of camera model """
return None
def camera_image(self):
""" Return bytes of camera image """
raise NotImplementedError()
@property
def state(self):
""" Returns the state of the entity. """
if self.is_recording:
return STATE_RECORDING
elif self.is_streaming:
return STATE_STREAMING
else:
return STATE_IDLE
@property
def state_attributes(self):
""" Returns optional state attributes. """
attr = {
ATTR_ENTITY_PICTURE: ENTITY_IMAGE_URL.format(
self.entity_id, time.time()),
}
if self.model:
attr['model_name'] = self.model
if self.brand:
attr['brand'] = self.brand
return attr

View File

@@ -0,0 +1,94 @@
"""
Support for IP Cameras.
This component provides basic support for IP cameras. For the basic support to
work you camera must support accessing a JPEG snapshot via a URL and you will
need to specify the "still_image_url" parameter which should be the location of
the JPEG image.
As part of the basic support the following features will be provided:
-MJPEG video streaming
-Saving a snapshot
-Recording(JPEG frame capture)
To use this component, add the following to your config/configuration.yaml:
camera:
platform: generic
name: Door Camera
username: YOUR_USERNAME
password: YOUR_PASSWORD
still_image_url: http://YOUR_CAMERA_IP_AND_PORT/image.jpg
VARIABLES:
These are the variables for the device_data array:
still_image_url
*Required
The URL your camera serves the image on.
Example: http://192.168.1.21:2112/
name
*Optional
This parameter allows you to override the name of your camera in homeassistant
username
*Optional
THe username for acessing your camera
password
*Optional
the password for accessing your camera
"""
import logging
from requests.auth import HTTPBasicAuth
from homeassistant.helpers import validate_config
from homeassistant.components.camera import DOMAIN
from homeassistant.components.camera import Camera
import requests
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Adds a generic IP Camera. """
if not validate_config({DOMAIN: config}, {DOMAIN: ['still_image_url']},
_LOGGER):
return None
add_devices_callback([GenericCamera(config)])
# pylint: disable=too-many-instance-attributes
class GenericCamera(Camera):
"""
A generic implementation of an IP camera that is reachable over a URL.
"""
def __init__(self, device_info):
super().__init__()
self._name = device_info.get('name', 'Generic Camera')
self._username = device_info.get('username')
self._password = device_info.get('password')
self._still_image_url = device_info['still_image_url']
def camera_image(self):
""" Return a still image reponse from the camera """
if self._username and self._password:
response = requests.get(
self._still_image_url,
auth=HTTPBasicAuth(self._username, self._password))
else:
response = requests.get(self._still_image_url)
return response.content
@property
def name(self):
""" Return the name of this device """
return self._name

View File

@@ -1,275 +0,0 @@
"""
homeassistant.components.chromecast
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to interact with Chromecasts.
"""
import logging
import homeassistant as ha
import homeassistant.util as util
import homeassistant.components as components
DOMAIN = 'chromecast'
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
STATE_NO_APP = 'no_app'
ATTR_HOST = 'host'
ATTR_STATE = 'state'
ATTR_OPTIONS = 'options'
ATTR_MEDIA_STATE = 'media_state'
ATTR_MEDIA_CONTENT_ID = 'media_content_id'
ATTR_MEDIA_TITLE = 'media_title'
ATTR_MEDIA_ARTIST = 'media_artist'
ATTR_MEDIA_ALBUM = 'media_album'
ATTR_MEDIA_IMAGE_URL = 'media_image_url'
ATTR_MEDIA_VOLUME = 'media_volume'
ATTR_MEDIA_DURATION = 'media_duration'
MEDIA_STATE_UNKNOWN = 'unknown'
MEDIA_STATE_PLAYING = 'playing'
MEDIA_STATE_STOPPED = 'stopped'
def is_on(statemachine, entity_id=None):
""" Returns true if specified ChromeCast entity_id is on.
Will check all chromecasts if no entity_id specified. """
entity_ids = [entity_id] if entity_id \
else util.filter_entity_ids(statemachine.entity_ids, DOMAIN)
return any(not statemachine.is_state(entity_id, STATE_NO_APP)
for entity_id in entity_ids)
def turn_off(bus, entity_id=None):
""" Will turn off specified Chromecast or all. """
data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
bus.call_service(DOMAIN, components.SERVICE_TURN_OFF, data)
def volume_up(bus, entity_id=None):
""" Send the chromecast the command for volume up. """
data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
bus.call_service(DOMAIN, components.SERVICE_VOLUME_UP, data)
def volume_down(bus, entity_id=None):
""" Send the chromecast the command for volume down. """
data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
bus.call_service(DOMAIN, components.SERVICE_VOLUME_DOWN, data)
def media_play_pause(bus, entity_id=None):
""" Send the chromecast the command for play/pause. """
data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
bus.call_service(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE, data)
def media_next_track(bus, entity_id=None):
""" Send the chromecast the command for next track. """
data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
bus.call_service(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK, data)
def media_prev_track(bus, entity_id=None):
""" Send the chromecast the command for prev track. """
data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
bus.call_service(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK, data)
# pylint: disable=too-many-locals, too-many-branches
def setup(bus, statemachine):
""" Listen for chromecast events. """
logger = logging.getLogger(__name__)
try:
import pychromecast
except ImportError:
logger.exception(("Failed to import pychromecast. "
"Did you maybe not install the 'pychromecast' "
"dependency?"))
return False
logger.info("Scanning for Chromecasts")
hosts = pychromecast.discover_chromecasts()
casts = {}
for host in hosts:
try:
cast = pychromecast.PyChromecast(host)
entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(
util.slugify(cast.device.friendly_name)),
casts.keys())
casts[entity_id] = cast
except pychromecast.ConnectionError:
pass
if not casts:
logger.error("Could not find Chromecasts")
return False
def update_chromecast_state(entity_id, chromecast):
""" Retrieve state of Chromecast and update statemachine. """
chromecast.refresh()
status = chromecast.app
state_attr = {ATTR_HOST: chromecast.host,
components.ATTR_FRIENDLY_NAME:
chromecast.device.friendly_name}
if status and status.app_id != pychromecast.APP_ID['HOME']:
state = status.app_id
ramp = chromecast.get_protocol(pychromecast.PROTOCOL_RAMP)
if ramp and ramp.state != pychromecast.RAMP_STATE_UNKNOWN:
if ramp.state == pychromecast.RAMP_STATE_PLAYING:
state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_PLAYING
else:
state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_STOPPED
if ramp.content_id:
state_attr[ATTR_MEDIA_CONTENT_ID] = ramp.content_id
if ramp.title:
state_attr[ATTR_MEDIA_TITLE] = ramp.title
if ramp.artist:
state_attr[ATTR_MEDIA_ARTIST] = ramp.artist
if ramp.album:
state_attr[ATTR_MEDIA_ALBUM] = ramp.album
if ramp.image_url:
state_attr[ATTR_MEDIA_IMAGE_URL] = ramp.image_url
if ramp.duration:
state_attr[ATTR_MEDIA_DURATION] = ramp.duration
state_attr[ATTR_MEDIA_VOLUME] = ramp.volume
else:
state = STATE_NO_APP
statemachine.set_state(entity_id, state, state_attr)
def update_chromecast_states(time): # pylint: disable=unused-argument
""" Updates all chromecast states. """
logger.info("Updating Chromecast status")
for entity_id, cast in casts.items():
update_chromecast_state(entity_id, cast)
def _service_to_entities(service):
""" Helper method to get entities from service. """
entity_ids = components.extract_entity_ids(statemachine, service)
if entity_ids:
for entity_id in entity_ids:
cast = casts.get(entity_id)
if cast:
yield entity_id, cast
else:
for item in casts.items():
yield item
def turn_off_service(service):
""" Service to exit any running app on the specified ChromeCast and
shows idle screen. Will quit all ChromeCasts if nothing specified.
"""
for entity_id, cast in _service_to_entities(service):
cast.quit_app()
update_chromecast_state(entity_id, cast)
def volume_up_service(service):
""" Service to send the chromecast the command for volume up. """
for _, cast in _service_to_entities(service):
ramp = cast.get_protocol(pychromecast.PROTOCOL_RAMP)
if ramp:
ramp.volume_up()
def volume_down_service(service):
""" Service to send the chromecast the command for volume down. """
for _, cast in _service_to_entities(service):
ramp = cast.get_protocol(pychromecast.PROTOCOL_RAMP)
if ramp:
ramp.volume_down()
def media_play_pause_service(service):
""" Service to send the chromecast the command for play/pause. """
for _, cast in _service_to_entities(service):
ramp = cast.get_protocol(pychromecast.PROTOCOL_RAMP)
if ramp:
ramp.playpause()
def media_next_track_service(service):
""" Service to send the chromecast the command for next track. """
for entity_id, cast in _service_to_entities(service):
ramp = cast.get_protocol(pychromecast.PROTOCOL_RAMP)
if ramp:
ramp.next()
update_chromecast_state(entity_id, cast)
def play_youtube_video_service(service, video_id):
""" Plays specified video_id on the Chromecast's YouTube channel. """
if video_id: # if service.data.get('video') returned None
for entity_id, cast in _service_to_entities(service):
pychromecast.play_youtube_video(video_id, cast.host)
update_chromecast_state(entity_id, cast)
ha.track_time_change(bus, update_chromecast_states)
bus.register_service(DOMAIN, components.SERVICE_TURN_OFF,
turn_off_service)
bus.register_service(DOMAIN, components.SERVICE_VOLUME_UP,
volume_up_service)
bus.register_service(DOMAIN, components.SERVICE_VOLUME_DOWN,
volume_down_service)
bus.register_service(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE,
media_play_pause_service)
bus.register_service(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK,
media_next_track_service)
bus.register_service(DOMAIN, "start_fireplace",
lambda service:
play_youtube_video_service(service, "eyU3bRy2x44"))
bus.register_service(DOMAIN, "start_epic_sax",
lambda service:
play_youtube_video_service(service, "kxopViU98Xo"))
bus.register_service(DOMAIN, SERVICE_YOUTUBE_VIDEO,
lambda service:
play_youtube_video_service(service,
service.data.get('video')))
update_chromecast_states(None)
return True

View File

@@ -0,0 +1,190 @@
"""
homeassistant.components.configurator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A component to allow pieces of code to request configuration from the user.
Initiate a request by calling the `request_config` method with a callback.
This will return a request id that has to be used for future calls.
A callback has to be provided to `request_config` which will be called when
the user has submitted configuration information.
"""
import logging
from homeassistant.helpers import generate_entity_id
from homeassistant.const import EVENT_TIME_CHANGED
DOMAIN = "configurator"
DEPENDENCIES = []
ENTITY_ID_FORMAT = DOMAIN + ".{}"
SERVICE_CONFIGURE = "configure"
STATE_CONFIGURE = "configure"
STATE_CONFIGURED = "configured"
ATTR_CONFIGURE_ID = "configure_id"
ATTR_DESCRIPTION = "description"
ATTR_DESCRIPTION_IMAGE = "description_image"
ATTR_SUBMIT_CAPTION = "submit_caption"
ATTR_FIELDS = "fields"
ATTR_ERRORS = "errors"
_REQUESTS = {}
_INSTANCES = {}
_LOGGER = logging.getLogger(__name__)
# pylint: disable=too-many-arguments
def request_config(
hass, name, callback, description=None, description_image=None,
submit_caption=None, fields=None):
""" Create a new request for config.
Will return an ID to be used for sequent calls. """
instance = _get_instance(hass)
request_id = instance.request_config(
name, callback,
description, description_image, submit_caption, fields)
_REQUESTS[request_id] = instance
return request_id
def notify_errors(request_id, error):
""" Add errors to a config request. """
try:
_REQUESTS[request_id].notify_errors(request_id, error)
except KeyError:
# If request_id does not exist
pass
def request_done(request_id):
""" Mark a config request as done. """
try:
_REQUESTS.pop(request_id).request_done(request_id)
except KeyError:
# If request_id does not exist
pass
def setup(hass, config):
""" Set up Configurator. """
return True
def _get_instance(hass):
""" Get an instance per hass object. """
try:
return _INSTANCES[hass]
except KeyError:
_INSTANCES[hass] = Configurator(hass)
if DOMAIN not in hass.config.components:
hass.config.components.append(DOMAIN)
return _INSTANCES[hass]
class Configurator(object):
"""
Class to keep track of current configuration requests.
"""
def __init__(self, hass):
self.hass = hass
self._cur_id = 0
self._requests = {}
hass.services.register(
DOMAIN, SERVICE_CONFIGURE, self.handle_service_call)
# pylint: disable=too-many-arguments
def request_config(
self, name, callback,
description, description_image, submit_caption, fields):
""" Setup a request for configuration. """
entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass)
if fields is None:
fields = []
request_id = self._generate_unique_id()
self._requests[request_id] = (entity_id, fields, callback)
data = {
ATTR_CONFIGURE_ID: request_id,
ATTR_FIELDS: fields,
}
data.update({
key: value for key, value in [
(ATTR_DESCRIPTION, description),
(ATTR_DESCRIPTION_IMAGE, description_image),
(ATTR_SUBMIT_CAPTION, submit_caption),
] if value is not None
})
self.hass.states.set(entity_id, STATE_CONFIGURE, data)
return request_id
def notify_errors(self, request_id, error):
""" Update the state with errors. """
if not self._validate_request_id(request_id):
return
entity_id = self._requests[request_id][0]
state = self.hass.states.get(entity_id)
new_data = state.attributes
new_data[ATTR_ERRORS] = error
self.hass.states.set(entity_id, STATE_CONFIGURE, new_data)
def request_done(self, request_id):
""" Remove the config request. """
if not self._validate_request_id(request_id):
return
entity_id = self._requests.pop(request_id)[0]
# If we remove the state right away, it will not be included with
# the result fo the service call (current design limitation).
# Instead, we will set it to configured to give as feedback but delete
# it shortly after so that it is deleted when the client updates.
self.hass.states.set(entity_id, STATE_CONFIGURED)
def deferred_remove(event):
""" Remove the request state. """
self.hass.states.remove(entity_id)
self.hass.bus.listen_once(EVENT_TIME_CHANGED, deferred_remove)
def handle_service_call(self, call):
""" Handle a configure service call. """
request_id = call.data.get(ATTR_CONFIGURE_ID)
if not self._validate_request_id(request_id):
return
# pylint: disable=unused-variable
entity_id, fields, callback = self._requests[request_id]
# field validation goes here?
callback(call.data.get(ATTR_FIELDS, {}))
def _generate_unique_id(self):
""" Generates a unique configurator id. """
self._cur_id += 1
return "{}-{}".format(id(self), self._cur_id)
def _validate_request_id(self, request_id):
""" Validate that the request belongs to this instance. """
return request_id in self._requests

View File

@@ -0,0 +1,70 @@
"""
homeassistant.components.conversation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to have conversations with Home Assistant.
This is more a proof of concept.
"""
import logging
import re
from homeassistant import core
from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
DOMAIN = "conversation"
DEPENDENCIES = []
SERVICE_PROCESS = "process"
ATTR_TEXT = "text"
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
def setup(hass, config):
""" Registers the process service. """
logger = logging.getLogger(__name__)
def process(service):
""" Parses text into commands for Home Assistant. """
if ATTR_TEXT not in service.data:
logger.error("Received process service call without a text")
return
text = service.data[ATTR_TEXT].lower()
match = REGEX_TURN_COMMAND.match(text)
if not match:
logger.error("Unable to process: %s", text)
return
name, command = match.groups()
entity_ids = [
state.entity_id for state in hass.states.all()
if state.attributes.get(ATTR_FRIENDLY_NAME, "").lower() == name]
if not entity_ids:
logger.error(
"Could not find entity id %s from text %s", name, text)
return
if command == 'on':
hass.services.call(core.DOMAIN, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity_ids,
}, blocking=True)
elif command == 'off':
hass.services.call(core.DOMAIN, SERVICE_TURN_OFF, {
ATTR_ENTITY_ID: entity_ids,
}, blocking=True)
else:
logger.error(
'Got unsupported command %s from text %s', command, text)
hass.services.register(DOMAIN, SERVICE_PROCESS, process)
return True

View File

@@ -0,0 +1,149 @@
"""
homeassistant.components.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sets up a demo environment that mimics interaction with devices.
"""
import time
import homeassistant.core as ha
import homeassistant.bootstrap as bootstrap
import homeassistant.loader as loader
from homeassistant.const import (
CONF_PLATFORM, ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME)
DOMAIN = "demo"
DEPENDENCIES = ['introduction', 'conversation']
COMPONENTS_WITH_DEMO_PLATFORM = [
'switch', 'light', 'thermostat', 'sensor', 'media_player', 'notify']
def setup(hass, config):
""" Setup a demo environment. """
group = loader.get_component('group')
configurator = loader.get_component('configurator')
config.setdefault(ha.DOMAIN, {})
config.setdefault(DOMAIN, {})
if config[DOMAIN].get('hide_demo_state') != 1:
hass.states.set('a.Demo_Mode', 'Enabled')
# Setup sun
if not hass.config.latitude:
hass.config.latitude = '32.87336'
if not hass.config.longitude:
hass.config.longitude = '117.22743'
bootstrap.setup_component(hass, 'sun')
# Setup demo platforms
for component in COMPONENTS_WITH_DEMO_PLATFORM:
bootstrap.setup_component(
hass, component, {component: {CONF_PLATFORM: 'demo'}})
# Setup room groups
lights = hass.states.entity_ids('light')
switches = hass.states.entity_ids('switch')
media_players = sorted(hass.states.entity_ids('media_player'))
group.setup_group(hass, 'living room', [lights[0], lights[1], switches[0],
media_players[1]])
group.setup_group(hass, 'bedroom', [lights[2], switches[1],
media_players[0]])
# Setup IP Camera
bootstrap.setup_component(
hass, 'camera',
{'camera': {
'platform': 'generic',
'name': 'IP Camera',
'still_image_url': 'http://194.218.96.92/jpg/image.jpg',
}})
# Setup scripts
bootstrap.setup_component(
hass, 'script',
{'script': {
'demo': {
'alias': 'Demo {}'.format(lights[0]),
'sequence': [{
'execute_service': 'light.turn_off',
'service_data': {ATTR_ENTITY_ID: lights[0]}
}, {
'delay': {'seconds': 5}
}, {
'execute_service': 'light.turn_on',
'service_data': {ATTR_ENTITY_ID: lights[0]}
}, {
'delay': {'seconds': 5}
}, {
'execute_service': 'light.turn_off',
'service_data': {ATTR_ENTITY_ID: lights[0]}
}]
}}})
# Setup scenes
bootstrap.setup_component(
hass, 'scene',
{'scene': [
{'name': 'Romantic lights',
'entities': {
lights[0]: True,
lights[1]: {'state': 'on', 'xy_color': [0.33, 0.66],
'brightness': 200},
}},
{'name': 'Switch on and off',
'entities': {
switches[0]: True,
switches[1]: False,
}},
]})
# Setup fake device tracker
hass.states.set("device_tracker.paulus", "home",
{ATTR_ENTITY_PICTURE:
"http://graph.facebook.com/297400035/picture",
ATTR_FRIENDLY_NAME: 'Paulus'})
hass.states.set("device_tracker.anne_therese", "not_home",
{ATTR_FRIENDLY_NAME: 'Anne Therese'})
hass.states.set("group.all_devices", "home",
{
"auto": True,
ATTR_ENTITY_ID: [
"device_tracker.paulus",
"device_tracker.anne_therese"
]
})
# Setup configurator
configurator_ids = []
def hue_configuration_callback(data):
""" Fake callback, mark config as done. """
time.sleep(2)
# First time it is called, pretend it failed.
if len(configurator_ids) == 1:
configurator.notify_errors(
configurator_ids[0],
"Failed to register, please try again.")
configurator_ids.append(0)
else:
configurator.request_done(configurator_ids[0])
request_id = configurator.request_config(
hass, "Philips Hue", hue_configuration_callback,
description=("Press the button on the bridge to register Philips Hue "
"with Home Assistant."),
description_image="/static/images/config_philips_hue.jpg",
submit_caption="I have pressed the button"
)
configurator_ids.append(request_id)
return True

View File

@@ -6,30 +6,44 @@ Provides functionality to turn on lights based on
the state of the sun and devices.
"""
import logging
from datetime import datetime, timedelta
from datetime import timedelta
import homeassistant as ha
import homeassistant.util as util
import homeassistant.components as components
from homeassistant.helpers.event import track_point_in_time, track_state_change
import homeassistant.util.dt as dt_util
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
from . import light, sun, device_tracker, group
DOMAIN = "device_sun_light_trigger"
DEPENDENCIES = ['light', 'device_tracker', 'group', 'sun']
LIGHT_TRANSITION_TIME = timedelta(minutes=15)
# Light profile to be used if none given
LIGHT_PROFILE = 'relax'
CONF_LIGHT_PROFILE = 'light_profile'
CONF_LIGHT_GROUP = 'light_group'
CONF_DEVICE_GROUP = 'device_group'
# pylint: disable=too-many-branches
def setup(bus, statemachine,
light_group=light.GROUP_NAME_ALL_LIGHTS,
light_profile=LIGHT_PROFILE):
def setup(hass, config):
""" Triggers to turn lights on or off based on device precense. """
disable_turn_off = 'disable_turn_off' in config[DOMAIN]
light_group = config[DOMAIN].get(CONF_LIGHT_GROUP,
light.ENTITY_ID_ALL_LIGHTS)
light_profile = config[DOMAIN].get(CONF_LIGHT_PROFILE, LIGHT_PROFILE)
device_group = config[DOMAIN].get(CONF_DEVICE_GROUP,
device_tracker.ENTITY_ID_ALL_DEVICES)
logger = logging.getLogger(__name__)
device_entity_ids = util.filter_entity_ids(statemachine.entity_ids,
device_tracker.DOMAIN)
device_entity_ids = group.get_entity_ids(hass, device_group,
device_tracker.DOMAIN)
if not device_entity_ids:
logger.error("No devices found to track")
@@ -37,8 +51,7 @@ def setup(bus, statemachine,
return False
# Get the light IDs from the specified group
light_ids = util.filter_entity_ids(
group.get_entity_ids(statemachine, light_group), light.DOMAIN)
light_ids = group.get_entity_ids(hass, light_group, light.DOMAIN)
if not light_ids:
logger.error("No lights found to turn on ")
@@ -48,14 +61,13 @@ def setup(bus, statemachine,
def calc_time_for_light_when_sunset():
""" Calculates the time when to start fading lights in when sun sets.
Returns None if no next_setting data available. """
next_setting = sun.next_setting(statemachine)
next_setting = sun.next_setting(hass)
if next_setting:
return (next_setting - LIGHT_TRANSITION_TIME * len(light_ids))
return next_setting - LIGHT_TRANSITION_TIME * len(light_ids)
else:
return None
# pylint: disable=unused-argument
def schedule_light_on_sun_rise(entity, old_state, new_state):
"""The moment sun sets we want to have all the lights on.
We will schedule to have each light start after one another
@@ -64,10 +76,9 @@ def setup(bus, statemachine,
def turn_light_on_before_sunset(light_id):
""" Helper function to turn on lights slowly if there
are devices home and the light is not on yet. """
if (device_tracker.is_on(statemachine) and
not light.is_on(statemachine, light_id)):
if device_tracker.is_on(hass) and not light.is_on(hass, light_id):
light.turn_on(bus, light_id,
light.turn_on(hass, light_id,
transition=LIGHT_TRANSITION_TIME.seconds,
profile=light_profile)
@@ -81,58 +92,55 @@ def setup(bus, statemachine,
if start_point:
for index, light_id in enumerate(light_ids):
ha.track_point_in_time(bus, turn_on(light_id),
(start_point +
index * LIGHT_TRANSITION_TIME))
track_point_in_time(
hass, turn_on(light_id),
(start_point + index * LIGHT_TRANSITION_TIME))
# Track every time sun rises so we can schedule a time-based
# pre-sun set event
ha.track_state_change(bus, sun.ENTITY_ID,
schedule_light_on_sun_rise,
sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON)
track_state_change(hass, sun.ENTITY_ID, schedule_light_on_sun_rise,
sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON)
# If the sun is already above horizon
# schedule the time-based pre-sun set event
if sun.is_on(statemachine):
if sun.is_on(hass):
schedule_light_on_sun_rise(None, None, None)
def check_light_on_dev_state_change(entity, old_state, new_state):
""" Function to handle tracked device state changes. """
lights_are_on = group.is_on(statemachine, light_group)
lights_are_on = group.is_on(hass, light_group)
light_needed = not (lights_are_on or sun.is_on(statemachine))
light_needed = not (lights_are_on or sun.is_on(hass))
# Specific device came home ?
if (entity != device_tracker.ENTITY_ID_ALL_DEVICES and
new_state.state == components.STATE_HOME):
if entity != device_tracker.ENTITY_ID_ALL_DEVICES and \
new_state.state == STATE_HOME:
# These variables are needed for the elif check
now = datetime.now()
now = dt_util.now()
start_point = calc_time_for_light_when_sunset()
# Do we need lights?
if light_needed:
logger.info(
"Home coming event for {}. Turning lights on".
format(entity))
"Home coming event for %s. Turning lights on", entity)
light.turn_on(bus, light_ids,
profile=light_profile)
light.turn_on(hass, light_ids, profile=light_profile)
# Are we in the time span were we would turn on the lights
# if someone would be home?
# Check this by seeing if current time is later then the point
# in time when we would start putting the lights on.
elif (start_point and
start_point < now < sun.next_setting(statemachine)):
start_point < now < sun.next_setting(hass)):
# Check for every light if it would be on if someone was home
# when the fading in started and turn it on if so
for index, light_id in enumerate(light_ids):
if now > start_point + index * LIGHT_TRANSITION_TIME:
light.turn_on(bus, light_id)
light.turn_on(hass, light_id)
else:
# If this light didn't happen to be turned on yet so
@@ -140,22 +148,23 @@ def setup(bus, statemachine,
break
# Did all devices leave the house?
elif (entity == device_tracker.ENTITY_ID_ALL_DEVICES and
new_state.state == components.STATE_NOT_HOME and lights_are_on):
elif (entity == device_group and
new_state.state == STATE_NOT_HOME and lights_are_on and
not disable_turn_off):
logger.info(
"Everyone has left but there are devices on. Turning them off")
"Everyone has left but there are lights on. Turning them off")
light.turn_off(bus)
light.turn_off(hass, light_ids)
# Track home coming of each seperate device
for entity in device_entity_ids:
ha.track_state_change(bus, entity, check_light_on_dev_state_change,
components.STATE_NOT_HOME, components.STATE_HOME)
# Track home coming of each device
track_state_change(
hass, device_entity_ids, check_light_on_dev_state_change,
STATE_NOT_HOME, STATE_HOME)
# Track when all devices are gone to shut down lights
ha.track_state_change(bus, device_tracker.ENTITY_ID_ALL_DEVICES,
check_light_on_dev_state_change,
components.STATE_HOME, components.STATE_NOT_HOME)
track_state_change(
hass, device_group, check_light_on_dev_state_change,
STATE_HOME, STATE_NOT_HOME)
return True

View File

@@ -1,458 +0,0 @@
"""
homeassistant.components.tracker
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to keep track of devices.
"""
import logging
import threading
import os
import csv
import re
import json
from datetime import datetime, timedelta
import requests
import homeassistant as ha
import homeassistant.util as util
import homeassistant.components as components
from homeassistant.components import group
DOMAIN = "device_tracker"
SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv"
GROUP_NAME_ALL_DEVICES = 'all_tracked_devices'
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format(
GROUP_NAME_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_SPAN_FOR_ERROR_IN_SCANNING = timedelta(minutes=3)
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
# Filename to save known devices to
KNOWN_DEVICES_FILE = "known_devices.csv"
def is_on(statemachine, entity_id=None):
""" Returns if any or specified device is home. """
entity = entity_id or ENTITY_ID_ALL_DEVICES
return statemachine.is_state(entity, components.STATE_HOME)
# pylint: disable=too-many-instance-attributes
class DeviceTracker(object):
""" Class that tracks which devices are home and which are not. """
def __init__(self, bus, statemachine, device_scanner, error_scanning=None):
self.statemachine = statemachine
self.bus = bus
self.device_scanner = device_scanner
self.error_scanning = error_scanning or TIME_SPAN_FOR_ERROR_IN_SCANNING
self.logger = logging.getLogger(__name__)
self.lock = threading.Lock()
# Dictionary to keep track of known devices and devices we track
self.known_devices = {}
# Did we encounter an invalid known devices file
self.invalid_known_devices_file = False
self._read_known_devices_file()
# Wrap it in a func instead of lambda so it can be identified in
# the bus by its __name__ attribute.
def update_device_state(time): # pylint: disable=unused-argument
""" Triggers update of the device states. """
self.update_devices()
ha.track_time_change(bus, update_device_state)
bus.register_service(DOMAIN,
SERVICE_DEVICE_TRACKER_RELOAD,
lambda service: self._read_known_devices_file())
self.update_devices()
group.setup(bus, statemachine, GROUP_NAME_ALL_DEVICES,
list(self.device_entity_ids))
@property
def device_entity_ids(self):
""" Returns a set containing all device entity ids
that are being tracked. """
return set([self.known_devices[device]['entity_id'] for device
in self.known_devices
if self.known_devices[device]['track']])
def update_devices(self, found_devices=None):
""" Update device states based on the found devices. """
self.lock.acquire()
found_devices = found_devices or self.device_scanner.scan_devices()
now = datetime.now()
known_dev = self.known_devices
temp_tracking_devices = [device for device in known_dev
if known_dev[device]['track']]
for device in found_devices:
# Are we tracking this device?
if device in temp_tracking_devices:
temp_tracking_devices.remove(device)
known_dev[device]['last_seen'] = now
self.statemachine.set_state(
known_dev[device]['entity_id'], components.STATE_HOME)
# For all devices we did not find, set state to NH
# But only if they have been gone for longer then the error time span
# Because we do not want to have stuff happening when the device does
# not show up for 1 scan beacuse of reboot etc
for device in temp_tracking_devices:
if (now - known_dev[device]['last_seen'] > self.error_scanning):
self.statemachine.set_state(known_dev[device]['entity_id'],
components.STATE_NOT_HOME)
# If we come along any unknown devices we will write them to the
# known devices file but only if we did not encounter an invalid
# known devices file
if not self.invalid_known_devices_file:
unknown_devices = [device for device in found_devices
if device not in known_dev]
if unknown_devices:
try:
# If file does not exist we will write the header too
is_new_file = not os.path.isfile(KNOWN_DEVICES_FILE)
with open(KNOWN_DEVICES_FILE, 'a') as outp:
self.logger.info((
"DeviceTracker:Found {} new devices,"
" updating {}").format(len(unknown_devices),
KNOWN_DEVICES_FILE))
writer = csv.writer(outp)
if is_new_file:
writer.writerow(("device", "name", "track"))
for device in unknown_devices:
# See if the device scanner knows the name
# else defaults to unknown device
name = (self.device_scanner.get_device_name(device)
or "unknown_device")
writer.writerow((device, name, 0))
known_dev[device] = {'name': name,
'track': False}
except IOError:
self.logger.exception((
"DeviceTracker:Error updating {}"
"with {} new devices").format(
KNOWN_DEVICES_FILE, len(unknown_devices)))
self.lock.release()
def _read_known_devices_file(self):
""" Parse and process the known devices file. """
# Read known devices if file exists
if os.path.isfile(KNOWN_DEVICES_FILE):
self.lock.acquire()
known_devices = {}
with open(KNOWN_DEVICES_FILE) as inp:
default_last_seen = datetime(1990, 1, 1)
# Temp variable to keep track of which entity ids we use
# so we can ensure we have unique entity ids.
used_entity_ids = []
try:
for row in csv.DictReader(inp):
device = row['device']
row['track'] = True if row['track'] == '1' else False
# If we track this device setup tracking variables
if row['track']:
row['last_seen'] = default_last_seen
# Make sure that each device is mapped
# to a unique entity_id name
name = util.slugify(row['name']) if row['name'] \
else "unnamed_device"
entity_id = ENTITY_ID_FORMAT.format(name)
tries = 1
while entity_id in used_entity_ids:
tries += 1
suffix = "_{}".format(tries)
entity_id = ENTITY_ID_FORMAT.format(
name + suffix)
row['entity_id'] = entity_id
used_entity_ids.append(entity_id)
known_devices[device] = row
if not known_devices:
self.logger.warning(
"No devices to track. Please update {}.".format(
KNOWN_DEVICES_FILE))
# Remove entities that are no longer maintained
new_entity_ids = set([known_devices[device]['entity_id']
for device in known_devices
if known_devices[device]['track']])
for entity_id in \
self.device_entity_ids - new_entity_ids:
self.logger.info(
"DeviceTracker:Removing entity {}".format(
entity_id))
self.statemachine.remove_entity(entity_id)
# File parsed, warnings given if necessary
# entities cleaned up, make it available
self.known_devices = known_devices
self.logger.info(
"DeviceTracker:Loaded devices from {}".format(
KNOWN_DEVICES_FILE))
except KeyError:
self.invalid_known_devices_file = True
self.logger.warning((
"Invalid {} found. "
"We won't update it with new found devices.").
format(KNOWN_DEVICES_FILE))
finally:
self.lock.release()
class TomatoDeviceScanner(object):
""" This class queries a wireless router running Tomato firmware
for connected devices.
A description of the Tomato API can be found on
http://paulusschoutsen.nl/blog/2013/10/tomato-api-documentation/
"""
def __init__(self, host, username, password, http_id):
self.req = requests.Request('POST',
'http://{}/update.cgi'.format(host),
data={'_http_id': http_id,
'exec': 'devlist'},
auth=requests.auth.HTTPBasicAuth(
username, password)).prepare()
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
self.logger = logging.getLogger(__name__)
self.lock = threading.Lock()
self.date_updated = None
self.last_results = {"wldev": [], "dhcpd_lease": []}
self.success_init = self._update_tomato_info()
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
self._update_tomato_info()
return [item[1] for item in self.last_results['wldev']]
def get_device_name(self, device):
""" Returns the name of the given device or None if we don't know. """
# Make sure there are results
if not self.date_updated:
self._update_tomato_info()
filter_named = [item[0] for item in self.last_results['dhcpd_lease']
if item[2] == device]
if not filter_named or not filter_named[0]:
return None
else:
return filter_named[0]
def _update_tomato_info(self):
""" Ensures the information from the Tomato router is up to date.
Returns boolean if scanning successful. """
self.lock.acquire()
# if date_updated is None or the date is too old we scan for new data
if (not self.date_updated or datetime.now() - self.date_updated >
MIN_TIME_BETWEEN_SCANS):
self.logger.info("Tomato:Scanning")
try:
response = requests.Session().send(self.req, timeout=3)
# Calling and parsing the Tomato api here. We only need the
# wldev and dhcpd_lease values. For API description see:
# http://paulusschoutsen.nl/
# blog/2013/10/tomato-api-documentation/
if response.status_code == 200:
for param, value in \
self.parse_api_pattern.findall(response.text):
if param == 'wldev' or param == 'dhcpd_lease':
self.last_results[param] = \
json.loads(value.replace("'", '"'))
self.date_updated = datetime.now()
return True
elif response.status_code == 401:
# Authentication error
self.logger.exception((
"Tomato:Failed to authenticate, "
"please check your username and password"))
return False
except requests.exceptions.ConnectionError:
# We get this if we could not connect to the router or
# an invalid http_id was supplied
self.logger.exception((
"Tomato:Failed to connect to the router"
" or invalid http_id supplied"))
return False
except requests.exceptions.Timeout:
# We get this if we could not connect to the router or
# an invalid http_id was supplied
self.logger.exception(
"Tomato:Connection to the router timed out")
return False
except ValueError:
# If json decoder could not parse the response
self.logger.exception(
"Tomato:Failed to parse response from router")
return False
finally:
self.lock.release()
else:
# We acquired the lock before the IF check,
# release it before we return True
self.lock.release()
return True
class NetgearDeviceScanner(object):
""" This class queries a Netgear wireless router using the SOAP-api. """
def __init__(self, host, username, password):
self.logger = logging.getLogger(__name__)
self.date_updated = None
self.last_results = []
try:
import homeassistant.external.pynetgear as pynetgear
except ImportError:
self.logger.exception(
("Netgear:Failed to import pynetgear. "
"Did you maybe not cloned the git submodules?"))
self.success_init = False
return
self._api = pynetgear.Netgear(host, username, password)
self.lock = threading.Lock()
self.logger.info("Netgear:Logging in")
if self._api.login():
self.success_init = True
self._update_info()
else:
self.logger.error("Netgear:Failed to Login")
self.success_init = False
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
self._update_info()
return [device.mac for device in self.last_results]
def get_device_name(self, mac):
""" Returns the name of the given device or None if we don't know. """
# Make sure there are results
if not self.date_updated:
self._update_info()
filter_named = [device.name for device in self.last_results
if device.mac == mac]
if filter_named:
return filter_named[0]
else:
return None
def _update_info(self):
""" Retrieves latest information from the Netgear router.
Returns boolean if scanning successful. """
if not self.success_init:
return
with self.lock:
# if date_updated is None or the date is too old we scan for
# new data
if (not self.date_updated or datetime.now() - self.date_updated >
MIN_TIME_BETWEEN_SCANS):
self.logger.info("Netgear:Scanning")
self.last_results = self._api.get_attached_devices()
self.date_updated = datetime.now()
return
else:
return

View File

@@ -0,0 +1,347 @@
"""
homeassistant.components.tracker
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to keep track of devices.
"""
import logging
import threading
import os
import csv
from datetime import timedelta
from homeassistant.loader import get_component
from homeassistant.helpers import validate_config
from homeassistant.helpers.entity import _OVERWRITE
import homeassistant.util as util
import homeassistant.util.dt as dt_util
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
DOMAIN = "device_tracker"
DEPENDENCIES = []
SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv"
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)
# Filename to save known devices to
KNOWN_DEVICES_FILE = "known_devices.csv"
CONF_SECONDS = "interval_seconds"
DEFAULT_CONF_SECONDS = 12
TRACK_NEW_DEVICES = "track_new_devices"
_LOGGER = logging.getLogger(__name__)
def is_on(hass, entity_id=None):
""" Returns if any or specified device is home. """
entity = entity_id or ENTITY_ID_ALL_DEVICES
return hass.states.is_state(entity, STATE_HOME)
def setup(hass, config):
""" Sets up the device tracker. """
if not validate_config(config, {DOMAIN: [CONF_PLATFORM]}, _LOGGER):
return False
tracker_type = config[DOMAIN].get(CONF_PLATFORM)
tracker_implementation = get_component(
'device_tracker.{}'.format(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):
return
try:
found_devices = set(dev.upper() for dev in
self.device_scanner.scan_devices())
for device in self.tracked:
is_home = device in found_devices
self._update_state(now, device, is_home)
if is_home:
found_devices.remove(device)
# Did we find any devices that we didn't know about yet?
new_devices = found_devices - self.untracked_devices
if new_devices:
if not self.track_new_devices:
self.untracked_devices.update(new_devices)
self._update_known_devices_file(new_devices)
finally:
self.lock.release()
# 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)
# Return if no known devices file exists
if not os.path.isfile(known_dev_path):
return
self.lock.acquire()
self.untracked_devices.clear()
with open(known_dev_path) as inp:
# 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())
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)
self._track_device(device, row['name'])
# Update state_attr with latest from file
state_attr = {
ATTR_FRIENDLY_NAME: row['name']
}
if row['picture']:
state_attr[ATTR_ENTITY_PICTURE] = row['picture']
self.tracked[device]['state_attr'] = state_attr
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']
_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

View File

@@ -0,0 +1,149 @@
"""
homeassistant.components.device_tracker.actiontec
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning an Actiontec MI424WR
(Verizon FIOS) router for device presence.
This device tracker needs telnet to be enabled on the router.
Configuration:
To use the Actiontec tracker you will need to add something like the
following to your config/configuration.yaml
device_tracker:
platform: actiontec
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import logging
from datetime import timedelta
import re
import threading
import telnetlib
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
_LEASES_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' +
r'\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))')
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns a DD-WRT scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return None
scanner = ActiontecDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class ActiontecDeviceScanner(object):
""" This class queries a an actiontec router
for connected devices. Adapted from DD-WRT scanner.
"""
def __init__(self, config):
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.lock = threading.Lock()
self.last_results = {}
# Test the router is accessible
data = self.get_actiontec_data()
self.success_init = data is not None
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
self._update_info()
return [client['mac'] for client in self.last_results]
def get_device_name(self, device):
""" Returns the name of the given device or None if we don't know. """
if not self.last_results:
return None
for client in self.last_results:
if client['mac'] == device:
return client['ip']
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Ensures the information from the Actiontec MI424WR router is up
to date. Returns boolean if scanning successful. """
if not self.success_init:
return False
with self.lock:
# _LOGGER.info("Checking ARP")
data = self.get_actiontec_data()
if not data:
return False
active_clients = [client for client in data.values()]
self.last_results = active_clients
return True
def get_actiontec_data(self):
""" Retrieve data from Actiontec MI424WR and return parsed result. """
try:
telnet = telnetlib.Telnet(self.host)
telnet.read_until(b'Username: ')
telnet.write((self.username + '\n').encode('ascii'))
telnet.read_until(b'Password: ')
telnet.write((self.password + '\n').encode('ascii'))
prompt = telnet.read_until(
b'Wireless Broadband Router> ').split(b'\n')[-1]
telnet.write('firewall mac_cache_dump\n'.encode('ascii'))
telnet.write('\n'.encode('ascii'))
telnet.read_until(prompt)
leases_result = telnet.read_until(prompt).split(b'\n')[1:-1]
telnet.write('exit\n'.encode('ascii'))
except EOFError:
_LOGGER.exception("Unexpected response from router")
return
except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router," +
" is telnet enabled?")
return None
devices = {}
for lease in leases_result:
match = _LEASES_REGEX.search(lease.decode('utf-8'))
if match is not None:
devices[match.group('ip')] = {
'ip': match.group('ip'),
'mac': match.group('mac').upper()
}
return devices

View File

@@ -0,0 +1,167 @@
"""
homeassistant.components.device_tracker.asuswrt
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a ASUSWRT router for device
presence.
This device tracker needs telnet to be enabled on the router.
Configuration:
To use the ASUSWRT tracker you will need to add something like the following
to your config/configuration.yaml
device_tracker:
platform: asuswrt
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import logging
from datetime import timedelta
import re
import threading
import telnetlib
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
_LEASES_REGEX = re.compile(
r'\w+\s' +
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s' +
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
r'(?P<host>([^\s]+))')
_IP_NEIGH_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
r'\w+\s' +
r'\w+\s' +
r'(\w+\s(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s' +
r'(?P<status>(\w+))')
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns a DD-WRT scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return None
scanner = AsusWrtDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class AsusWrtDeviceScanner(object):
""" This class queries a router running ASUSWRT firmware
for connected devices. Adapted from DD-WRT scanner.
"""
def __init__(self, config):
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.lock = threading.Lock()
self.last_results = {}
# Test the router is accessible
data = self.get_asuswrt_data()
self.success_init = data is not None
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
self._update_info()
return [client['mac'] for client in self.last_results]
def get_device_name(self, device):
""" Returns the name of the given device or None if we don't know. """
if not self.last_results:
return None
for client in self.last_results:
if client['mac'] == device:
return client['host']
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Ensures the information from the ASUSWRT router is up to date.
Returns boolean if scanning successful. """
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Checking ARP")
data = self.get_asuswrt_data()
if not data:
return False
active_clients = [client for client in data.values() if
client['status'] == 'REACHABLE' or
client['status'] == 'DELAY' or
client['status'] == 'STALE']
self.last_results = active_clients
return True
def get_asuswrt_data(self):
""" Retrieve data from ASUSWRT and return parsed result. """
try:
telnet = telnetlib.Telnet(self.host)
telnet.read_until(b'login: ')
telnet.write((self.username + '\n').encode('ascii'))
telnet.read_until(b'Password: ')
telnet.write((self.password + '\n').encode('ascii'))
prompt_string = telnet.read_until(b'#').split(b'\n')[-1]
telnet.write('ip neigh\n'.encode('ascii'))
neighbors = telnet.read_until(prompt_string).split(b'\n')[1:-1]
telnet.write('cat /var/lib/misc/dnsmasq.leases\n'.encode('ascii'))
leases_result = telnet.read_until(prompt_string).split(b'\n')[1:-1]
telnet.write('exit\n'.encode('ascii'))
except EOFError:
_LOGGER.exception("Unexpected response from router")
return
except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router," +
" is telnet enabled?")
return
devices = {}
for lease in leases_result:
match = _LEASES_REGEX.search(lease.decode('utf-8'))
devices[match.group('ip')] = {
'ip': match.group('ip'),
'mac': match.group('mac').upper(),
'host': match.group('host'),
'status': ''
}
for neighbor in neighbors:
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
if match.group('ip') in devices:
devices[match.group('ip')]['status'] = match.group('status')
return devices

View File

@@ -0,0 +1,191 @@
"""
homeassistant.components.device_tracker.ddwrt
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a DD-WRT router for device
presence.
Configuration:
To use the DD-WRT tracker you will need to add something like the following
to your config/configuration.yaml
device_tracker:
platform: ddwrt
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import logging
from datetime import timedelta
import re
import threading
import requests
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
_DDWRT_DATA_REGEX = re.compile(r'\{(\w+)::([^\}]*)\}')
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns a DD-WRT scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return None
scanner = DdWrtDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
# pylint: disable=too-many-instance-attributes
class DdWrtDeviceScanner(object):
""" This class queries a wireless router running DD-WRT firmware
for connected devices. Adapted from Tomato scanner.
"""
def __init__(self, config):
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.lock = threading.Lock()
self.last_results = {}
self.mac2name = None
# Test the router is accessible
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
data = self.get_ddwrt_data(url)
self.success_init = data is not None
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
self._update_info()
return self.last_results
def get_device_name(self, device):
""" Returns the name of the given device or None if we don't know. """
with self.lock:
# if not initialised and not already scanned and not found
if self.mac2name is None or device not in self.mac2name:
url = 'http://{}/Status_Lan.live.asp'.format(self.host)
data = self.get_ddwrt_data(url)
if not data:
return
dhcp_leases = data.get('dhcp_leases', None)
if dhcp_leases:
# remove leading and trailing single quotes
cleaned_str = dhcp_leases.strip().strip('"')
elements = cleaned_str.split('","')
num_clients = int(len(elements)/5)
self.mac2name = {}
for idx in range(0, num_clients):
# this is stupid but the data is a single array
# every 5 elements represents one hosts, the MAC
# is the third element and the name is the first
mac_index = (idx * 5) + 2
if mac_index < len(elements):
mac = elements[mac_index]
self.mac2name[mac] = elements[idx * 5]
return self.mac2name.get(device, None)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Ensures the information from the DD-WRT router is up to date.
Returns boolean if scanning successful. """
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Checking ARP")
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
data = self.get_ddwrt_data(url)
if not data:
return False
if data:
self.last_results = []
active_clients = data.get('active_wireless', None)
if active_clients:
# This is really lame, instead of using JSON the DD-WRT UI
# uses its own data format for some reason and then
# regex's out values so I guess I have to do the same,
# LAME!!!
# remove leading and trailing single quotes
clean_str = active_clients.strip().strip("'")
elements = clean_str.split("','")
num_clients = int(len(elements)/9)
for idx in range(0, num_clients):
# get every 9th element which is the MAC address
index = idx * 9
if index < len(elements):
self.last_results.append(elements[index])
return True
return False
def get_ddwrt_data(self, url):
""" Retrieve data from DD-WRT and return parsed result. """
try:
response = requests.get(
url,
auth=(self.username, self.password),
timeout=4)
except requests.exceptions.Timeout:
_LOGGER.exception("Connection to the router timed out")
return
if response.status_code == 200:
return _parse_ddwrt_response(response.text)
elif response.status_code == 401:
# Authentication error
_LOGGER.exception(
"Failed to authenticate, "
"please check your username and password")
return
else:
_LOGGER.error("Invalid response from ddwrt: %s", response)
def _parse_ddwrt_response(data_str):
""" Parse the DD-WRT data format. """
return {
key: val for key, val in _DDWRT_DATA_REGEX
.findall(data_str)}

View File

@@ -0,0 +1,184 @@
"""
homeassistant.components.device_tracker.luci
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a OpenWRT router for device
presence.
It's required that the luci RPC package is installed on the OpenWRT router:
# opkg install luci-mod-rpc
Configuration:
To use the Luci tracker you will need to add something like the following
to your config/configuration.yaml
device_tracker:
platform: luci
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import logging
import json
from datetime import timedelta
import re
import threading
import requests
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
def get_scanner(hass, config):
""" Validates config and returns a Luci scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return None
scanner = LuciDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
# pylint: disable=too-many-instance-attributes
class LuciDeviceScanner(object):
""" This class queries a wireless router running OpenWrt firmware
for connected devices. Adapted from Tomato scanner.
# opkg install luci-mod-rpc
for this to work on the router.
The API is described here:
http://luci.subsignal.org/trac/wiki/Documentation/JsonRpcHowTo
(Currently, we do only wifi iwscan, and no DHCP lease access.)
"""
def __init__(self, config):
host = config[CONF_HOST]
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
self.lock = threading.Lock()
self.last_results = {}
self.token = _get_token(host, username, password)
self.host = host
self.mac2name = None
self.success_init = self.token is not None
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
self._update_info()
return self.last_results
def get_device_name(self, device):
""" Returns the name of the given device or None if we don't know. """
with self.lock:
if self.mac2name is None:
url = 'http://{}/cgi-bin/luci/rpc/uci'.format(self.host)
result = _req_json_rpc(url, 'get_all', 'dhcp',
params={'auth': self.token})
if result:
hosts = [x for x in result.values()
if x['.type'] == 'host' and
'mac' in x and 'name' in x]
mac2name_list = [
(x['mac'].upper(), x['name']) for x in hosts]
self.mac2name = dict(mac2name_list)
else:
# Error, handled in the _req_json_rpc
return
return self.mac2name.get(device.upper(), None)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Ensures the information from the Luci router is up to date.
Returns boolean if scanning successful. """
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Checking ARP")
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
result = _req_json_rpc(url, 'net.arptable',
params={'auth': self.token})
if result:
self.last_results = []
for device_entry in result:
# Check if the Flags for each device contain
# NUD_REACHABLE and if so, add it to last_results
if int(device_entry['Flags'], 16) & 0x2:
self.last_results.append(device_entry['HW address'])
return True
return False
def _req_json_rpc(url, method, *args, **kwargs):
""" Perform one JSON RPC operation. """
data = json.dumps({'method': method, 'params': args})
try:
res = requests.post(url, data=data, timeout=5, **kwargs)
except requests.exceptions.Timeout:
_LOGGER.exception("Connection to the router timed out")
return
if res.status_code == 200:
try:
result = res.json()
except ValueError:
# If json decoder could not parse the response
_LOGGER.exception("Failed to parse response from luci")
return
try:
return result['result']
except KeyError:
_LOGGER.exception("No result in response from luci")
return
elif res.status_code == 401:
# Authentication error
_LOGGER.exception(
"Failed to authenticate, "
"please check your username and password")
return
else:
_LOGGER.error("Invalid response from luci: %s", res)
def _get_token(host, username, password):
""" Get authentication token for the given host+username+password """
url = 'http://{}/cgi-bin/luci/rpc/auth'.format(host)
return _req_json_rpc(url, 'login', username, password)

View File

@@ -0,0 +1,117 @@
"""
homeassistant.components.device_tracker.netgear
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a Netgear router for device
presence.
Configuration:
To use the Netgear tracker you will need to add something like the following
to your config/configuration.yaml
device_tracker:
platform: netgear
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import logging
from datetime import timedelta
import threading
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pynetgear==0.3']
def get_scanner(hass, config):
""" Validates config and returns a Netgear scanner. """
info = config[DOMAIN]
host = info.get(CONF_HOST)
username = info.get(CONF_USERNAME)
password = info.get(CONF_PASSWORD)
if password is not None and host is None:
_LOGGER.warning('Found username or password but no host')
return None
scanner = NetgearDeviceScanner(host, username, password)
return scanner if scanner.success_init else None
class NetgearDeviceScanner(object):
""" This class queries a Netgear wireless router using the SOAP-API. """
def __init__(self, host, username, password):
import pynetgear
self.last_results = []
self.lock = threading.Lock()
if host is None:
print("BIER")
self._api = pynetgear.Netgear()
elif username is None:
self._api = pynetgear.Netgear(password, host)
else:
self._api = pynetgear.Netgear(password, host, username)
_LOGGER.info("Logging in")
results = self._api.get_attached_devices()
self.success_init = results is not None
if self.success_init:
self.last_results = results
else:
_LOGGER.error("Failed to Login")
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
self._update_info()
return (device.mac for device in self.last_results)
def get_device_name(self, mac):
""" Returns the name of the given device or None if we don't know. """
try:
return next(device.name for device in self.last_results
if device.mac == mac)
except StopIteration:
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Retrieves latest information from the Netgear router.
Returns boolean if scanning successful. """
if not self.success_init:
return
with self.lock:
_LOGGER.info("Scanning")
self.last_results = self._api.get_attached_devices() or []

View File

@@ -0,0 +1,172 @@
"""
homeassistant.components.device_tracker.nmap
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a network with nmap.
Configuration:
To use the nmap tracker you will need to add something like the following
to your config/configuration.yaml
device_tracker:
platform: nmap_tracker
hosts: 192.168.1.1/24
Variables:
hosts
*Required
The IP addresses to scan in the network-prefix notation (192.168.1.1/24) or
the range notation (192.168.1.1-255).
"""
import logging
from datetime import timedelta
from collections import namedtuple
import subprocess
import re
try:
from libnmap.process import NmapProcess
from libnmap.parser import NmapParser, NmapParserException
LIB_LOADED = True
except ImportError:
LIB_LOADED = False
import homeassistant.util.dt as dt_util
from homeassistant.const import CONF_HOSTS
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle, convert
from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
# interval in minutes to exclude devices from a scan while they are home
CONF_HOME_INTERVAL = "home_interval"
REQUIREMENTS = ['python-libnmap==0.6.1']
def get_scanner(hass, config):
""" Validates config and returns a Nmap scanner. """
if not validate_config(config, {DOMAIN: [CONF_HOSTS]},
_LOGGER):
return None
if not LIB_LOADED:
_LOGGER.error("Error while importing dependency python-libnmap.")
return False
scanner = NmapDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
Device = namedtuple("Device", ["mac", "name", "ip", "last_update"])
def _arp(ip_address):
""" Get the MAC address for a given IP. """
cmd = ['arp', '-n', ip_address]
arp = subprocess.Popen(cmd, stdout=subprocess.PIPE)
out, _ = arp.communicate()
match = re.search(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})', str(out))
if match:
return match.group(0)
_LOGGER.info("No MAC address found for %s", ip_address)
return ''
class NmapDeviceScanner(object):
""" This class scans for devices using nmap """
def __init__(self, config):
self.last_results = []
self.hosts = config[CONF_HOSTS]
minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0)
self.home_interval = timedelta(minutes=minutes)
self.success_init = True
self._update_info()
_LOGGER.info("nmap scanner initialized")
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
self._update_info()
return [device.mac for device in self.last_results]
def get_device_name(self, mac):
""" Returns the name of the given device or None if we don't know. """
filter_named = [device.name for device in self.last_results
if device.mac == mac]
if filter_named:
return filter_named[0]
else:
return None
def _parse_results(self, stdout):
""" Parses results from an nmap scan.
Returns True if successful, False otherwise. """
try:
results = NmapParser.parse(stdout)
now = dt_util.now()
self.last_results = []
for host in results.hosts:
if host.is_up():
if host.hostnames:
name = host.hostnames[0]
else:
name = host.ipv4
if host.mac:
mac = host.mac
else:
mac = _arp(host.ipv4)
if mac:
device = Device(mac.upper(), name, host.ipv4, now)
self.last_results.append(device)
_LOGGER.info("nmap scan successful")
return True
except NmapParserException as parse_exc:
_LOGGER.error("failed to parse nmap results: %s", parse_exc.msg)
self.last_results = []
return False
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Scans the network for devices.
Returns boolean if scanning successful. """
if not self.success_init:
return False
_LOGGER.info("Scanning")
options = "-F --host-timeout 5"
exclude_targets = set()
if self.home_interval:
now = dt_util.now()
for host in self.last_results:
if host.last_update + self.home_interval > now:
exclude_targets.add(host)
if len(exclude_targets) > 0:
target_list = [t.ip for t in exclude_targets]
options += " --exclude {}".format(",".join(target_list))
nmap = NmapProcess(targets=self.hosts, options=options)
nmap.run()
if nmap.rc == 0:
if self._parse_results(nmap.stdout):
self.last_results.extend(exclude_targets)
else:
self.last_results = []
_LOGGER.error(nmap.stderr)
return False

View File

@@ -0,0 +1,157 @@
"""
homeassistant.components.device_tracker.thomson
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a THOMSON router for device
presence.
This device tracker needs telnet to be enabled on the router.
Configuration:
To use the THOMSON tracker you will need to add something like the following
to your config/configuration.yaml
device_tracker:
platform: thomson
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import logging
from datetime import timedelta
import re
import threading
import telnetlib
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
_LOGGER = logging.getLogger(__name__)
_DEVICES_REGEX = re.compile(
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s' +
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+' +
r'(?P<status>([^\s]+))\s+' +
r'(?P<type>([^\s]+))\s+' +
r'(?P<intf>([^\s]+))\s+' +
r'(?P<hwintf>([^\s]+))\s+' +
r'(?P<host>([^\s]+))')
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns a THOMSON scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return None
scanner = ThomsonDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class ThomsonDeviceScanner(object):
""" This class queries a router running THOMSON firmware
for connected devices. Adapted from ASUSWRT scanner.
"""
def __init__(self, config):
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.lock = threading.Lock()
self.last_results = {}
# Test the router is accessible
data = self.get_thomson_data()
self.success_init = data is not None
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
self._update_info()
return [client['mac'] for client in self.last_results]
def get_device_name(self, device):
""" Returns the name of the given device
or None if we don't know. """
if not self.last_results:
return None
for client in self.last_results:
if client['mac'] == device:
return client['host']
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Ensures the information from the THOMSON router is up to date.
Returns boolean if scanning successful. """
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Checking ARP")
data = self.get_thomson_data()
if not data:
return False
# flag C stands for CONNECTED
active_clients = [client for client in data.values() if
client['status'].find('C') != -1]
self.last_results = active_clients
return True
def get_thomson_data(self):
""" Retrieve data from THOMSON and return parsed result. """
try:
telnet = telnetlib.Telnet(self.host)
telnet.read_until(b'Username : ')
telnet.write((self.username + '\r\n').encode('ascii'))
telnet.read_until(b'Password : ')
telnet.write((self.password + '\r\n').encode('ascii'))
telnet.read_until(b'=>')
telnet.write(('hostmgr list\r\n').encode('ascii'))
devices_result = telnet.read_until(b'=>').split(b'\r\n')
telnet.write('exit\r\n'.encode('ascii'))
except EOFError:
_LOGGER.exception("Unexpected response from router")
return
except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router," +
" is telnet enabled?")
return
devices = {}
for device in devices_result:
match = _DEVICES_REGEX.search(device.decode('utf-8'))
if match:
devices[match.group('ip')] = {
'ip': match.group('ip'),
'mac': match.group('mac').upper(),
'host': match.group('host'),
'status': match.group('status')
}
return devices

View File

@@ -0,0 +1,174 @@
"""
homeassistant.components.device_tracker.tomato
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a Tomato router for device
presence.
Configuration:
To use the Tomato tracker you will need to add something like the following
to your config/configuration.yaml
device_tracker:
platform: tomato
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
http_id: ABCDEFG
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
http_id
*Required
The value can be obtained by logging in to the Tomato admin interface and
search for http_id in the page source code.
"""
import logging
import json
from datetime import timedelta
import re
import threading
import requests
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
CONF_HTTP_ID = "http_id"
_LOGGER = logging.getLogger(__name__)
def get_scanner(hass, config):
""" Validates config and returns a Tomato scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME,
CONF_PASSWORD, CONF_HTTP_ID]},
_LOGGER):
return None
return TomatoDeviceScanner(config[DOMAIN])
class TomatoDeviceScanner(object):
""" This class queries a wireless router running Tomato firmware
for connected devices.
A description of the Tomato API can be found on
http://paulusschoutsen.nl/blog/2013/10/tomato-api-documentation/
"""
def __init__(self, config):
host, http_id = config[CONF_HOST], config[CONF_HTTP_ID]
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
self.req = requests.Request('POST',
'http://{}/update.cgi'.format(host),
data={'_http_id': http_id,
'exec': 'devlist'},
auth=requests.auth.HTTPBasicAuth(
username, password)).prepare()
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
self.logger = logging.getLogger("{}.{}".format(__name__, "Tomato"))
self.lock = threading.Lock()
self.last_results = {"wldev": [], "dhcpd_lease": []}
self.success_init = self._update_tomato_info()
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
self._update_tomato_info()
return [item[1] for item in self.last_results['wldev']]
def get_device_name(self, device):
""" Returns the name of the given device or None if we don't know. """
filter_named = [item[0] for item in self.last_results['dhcpd_lease']
if item[2] == device]
if not filter_named or not filter_named[0]:
return None
else:
return filter_named[0]
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_tomato_info(self):
""" Ensures the information from the Tomato router is up to date.
Returns boolean if scanning successful. """
with self.lock:
self.logger.info("Scanning")
try:
response = requests.Session().send(self.req, timeout=3)
# Calling and parsing the Tomato api here. We only need the
# wldev and dhcpd_lease values. For API description see:
# http://paulusschoutsen.nl/
# blog/2013/10/tomato-api-documentation/
if response.status_code == 200:
for param, value in \
self.parse_api_pattern.findall(response.text):
if param == 'wldev' or param == 'dhcpd_lease':
self.last_results[param] = \
json.loads(value.replace("'", '"'))
return True
elif response.status_code == 401:
# Authentication error
self.logger.exception((
"Failed to authenticate, "
"please check your username and password"))
return False
except requests.exceptions.ConnectionError:
# We get this if we could not connect to the router or
# an invalid http_id was supplied
self.logger.exception((
"Failed to connect to the router"
" or invalid http_id supplied"))
return False
except requests.exceptions.Timeout:
# We get this if we could not connect to the router or
# an invalid http_id was supplied
self.logger.exception(
"Connection to the router timed out")
return False
except ValueError:
# If json decoder could not parse the response
self.logger.exception(
"Failed to parse response from router")
return False

View File

@@ -0,0 +1,181 @@
"""
homeassistant.components.device_tracker.tplink
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a TP-Link router for device
presence.
Configuration:
To use the TP-Link tracker you will need to add something like the following
to your config/configuration.yaml
device_tracker:
platform: tplink
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import base64
import logging
from datetime import timedelta
import re
import threading
import requests
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
def get_scanner(hass, config):
""" Validates config and returns a TP-Link scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return None
scanner = Tplink2DeviceScanner(config[DOMAIN])
if not scanner.success_init:
scanner = TplinkDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class TplinkDeviceScanner(object):
""" This class queries a wireless router running TP-Link firmware
for connected devices.
"""
def __init__(self, config):
host = config[CONF_HOST]
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
self.parse_macs = re.compile('[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}-' +
'[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}')
self.host = host
self.username = username
self.password = password
self.last_results = {}
self.lock = threading.Lock()
self.success_init = self._update_info()
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
self._update_info()
return self.last_results
# pylint: disable=no-self-use
def get_device_name(self, device):
""" The TP-Link firmware doesn't save the name of the wireless
device. """
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Ensures the information from the TP-Link router is up to date.
Returns boolean if scanning successful. """
with self.lock:
_LOGGER.info("Loading wireless clients...")
url = 'http://{}/userRpm/WlanStationRpm.htm'.format(self.host)
referer = 'http://{}'.format(self.host)
page = requests.get(url, auth=(self.username, self.password),
headers={'referer': referer})
result = self.parse_macs.findall(page.text)
if result:
self.last_results = [mac.replace("-", ":") for mac in result]
return True
return False
class Tplink2DeviceScanner(TplinkDeviceScanner):
""" This class queries a wireless router running newer version of TP-Link
firmware for connected devices.
"""
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
self._update_info()
return self.last_results.keys()
# pylint: disable=no-self-use
def get_device_name(self, device):
""" The TP-Link firmware doesn't save the name of the wireless
device. """
return self.last_results.get(device)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Ensures the information from the TP-Link router is up to date.
Returns boolean if scanning successful. """
with self.lock:
_LOGGER.info("Loading wireless clients...")
url = 'http://{}/data/map_access_wireless_client_grid.json'\
.format(self.host)
referer = 'http://{}'.format(self.host)
# Router uses Authorization cookie instead of header
# Let's create the cookie
username_password = '{}:{}'.format(self.username, self.password)
b64_encoded_username_password = base64.b64encode(
username_password.encode('ascii')
).decode('ascii')
cookie = 'Authorization=Basic {}'\
.format(b64_encoded_username_password)
response = requests.post(url, headers={'referer': referer,
'cookie': cookie})
try:
result = response.json().get('data')
except ValueError:
_LOGGER.error("Router didn't respond with JSON. "
"Check if credentials are correct.")
return False
if result:
self.last_results = {
device['mac_addr'].replace('-', ':'): device['name']
for device in result
}
return True
return False

View File

@@ -0,0 +1,106 @@
"""
homeassistant.components.discovery
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Starts a service to scan in intervals for new devices.
Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered.
Knows which components handle certain types, will make sure they are
loaded before the EVENT_PLATFORM_DISCOVERED is fired.
"""
import logging
import threading
from homeassistant import bootstrap
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_PLATFORM_DISCOVERED,
ATTR_SERVICE, ATTR_DISCOVERED)
DOMAIN = "discovery"
DEPENDENCIES = []
REQUIREMENTS = ['netdisco==0.3']
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_HANDLERS = {
SERVICE_WEMO: "switch",
SERVICE_CAST: "media_player",
SERVICE_HUE: "light",
SERVICE_NETGEAR: 'device_tracker',
}
def listen(hass, service, callback):
"""
Setup listener for discovery of specific service.
Service can be a string or a list/tuple.
"""
if isinstance(service, str):
service = (service,)
else:
service = tuple(service)
def discovery_event_listener(event):
""" Listens for discovery events. """
if event.data[ATTR_SERVICE] in service:
callback(event.data[ATTR_SERVICE], event.data[ATTR_DISCOVERED])
hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener)
def setup(hass, config):
""" Starts a discovery service. """
logger = logging.getLogger(__name__)
from netdisco.service import DiscoveryService
# Disable zeroconf logging, it spams
logging.getLogger('zeroconf').setLevel(logging.CRITICAL)
lock = threading.Lock()
def new_service_listener(service, info):
""" Called when a new service is found. """
with lock:
logger.info("Found new service: %s %s", service, info)
component = SERVICE_HANDLERS.get(service)
# We do not know how to handle this service
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
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: service,
ATTR_DISCOVERED: info
})
def start_discovery(event):
""" Start discovering. """
netdisco = DiscoveryService(SCAN_INTERVAL)
netdisco.add_listener(new_service_listener)
netdisco.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_discovery)
return True

View File

@@ -9,18 +9,22 @@ import logging
import re
import threading
import homeassistant.util as util
from homeassistant.helpers import validate_config
from homeassistant.util import sanitize_filename
DOMAIN = "downloader"
DEPENDENCIES = []
SERVICE_DOWNLOAD_FILE = "download_file"
ATTR_URL = "url"
ATTR_SUBDIR = "subdir"
CONF_DOWNLOAD_DIR = 'download_dir'
# pylint: disable=too-many-branches
def setup(bus, download_path):
def setup(hass, config):
""" Listens for download events to download files. """
logger = logging.getLogger(__name__)
@@ -33,18 +37,23 @@ def setup(bus, download_path):
return False
if not validate_config(config, {DOMAIN: [CONF_DOWNLOAD_DIR]}, logger):
return False
download_path = config[DOMAIN][CONF_DOWNLOAD_DIR]
if not os.path.isdir(download_path):
logger.error(
("Download path {} does not exist. File Downloader not active.").
format(download_path))
"Download path %s does not exist. File Downloader not active.",
download_path)
return False
def download_file(service):
""" Starts thread to download file specified in the url. """
if not ATTR_URL in service.data:
if ATTR_URL not in service.data:
logger.error("Service called but 'url' parameter not specified.")
return
@@ -56,11 +65,11 @@ def setup(bus, download_path):
subdir = service.data.get(ATTR_SUBDIR)
if subdir:
subdir = util.sanitize_filename(subdir)
subdir = sanitize_filename(subdir)
final_path = None
req = requests.get(url, stream=True)
req = requests.get(url, stream=True, timeout=10)
if req.status_code == 200:
filename = None
@@ -80,7 +89,7 @@ def setup(bus, download_path):
filename = "ha_download"
# Remove stuff to ruin paths
filename = util.sanitize_filename(filename)
filename = sanitize_filename(filename)
# Do we want to download to subdir, create if needed
if subdir:
@@ -106,19 +115,16 @@ def setup(bus, download_path):
final_path = "{}_{}.{}".format(path, tries, ext)
logger.info("{} -> {}".format(
url, final_path))
logger.info("%s -> %s", url, final_path)
with open(final_path, 'wb') as fil:
for chunk in req.iter_content(1024):
fil.write(chunk)
logger.info("Downloading of {} done".format(
url))
logger.info("Downloading of %s done", url)
except requests.exceptions.ConnectionError:
logger.exception("ConnectionError occured for {}".
format(url))
logger.exception("ConnectionError occured for %s", url)
# Remove file if we started downloading but failed
if final_path and os.path.isfile(final_path):
@@ -126,7 +132,7 @@ def setup(bus, download_path):
threading.Thread(target=do_download).start()
bus.register_service(DOMAIN, SERVICE_DOWNLOAD_FILE,
download_file)
hass.services.register(DOMAIN, SERVICE_DOWNLOAD_FILE,
download_file)
return True

View File

@@ -0,0 +1,85 @@
"""
homeassistant.components.frontend
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides a frontend for Home Assistant.
"""
import re
import os
import logging
from . import version
import homeassistant.util as util
from homeassistant.const import URL_ROOT, HTTP_OK
DOMAIN = 'frontend'
DEPENDENCIES = ['api']
INDEX_PATH = os.path.join(os.path.dirname(__file__), 'index.html.template')
_LOGGER = logging.getLogger(__name__)
FRONTEND_URLS = [
URL_ROOT, '/logbook', '/history', '/devService', '/devState', '/devEvent']
STATES_URL = re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)')
def setup(hass, config):
""" Setup serving the frontend. """
if 'http' not in hass.config.components:
_LOGGER.error('Dependency http is not loaded')
return False
for url in FRONTEND_URLS:
hass.http.register_path('GET', url, _handle_get_root, False)
hass.http.register_path('GET', STATES_URL, _handle_get_root, False)
# Static files
hass.http.register_path(
'GET', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
_handle_get_static, False)
hass.http.register_path(
'HEAD', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
_handle_get_static, False)
return True
def _handle_get_root(handler, path_match, data):
""" Renders the debug interface. """
handler.send_response(HTTP_OK)
handler.send_header('Content-type', 'text/html; charset=utf-8')
handler.end_headers()
if handler.server.development:
app_url = "home-assistant-polymer/src/home-assistant.html"
else:
app_url = "frontend-{}.html".format(version.VERSION)
# auto login if no password was set, else check api_password param
auth = ('no_password_set' if handler.server.no_password_set
else data.get('api_password', ''))
with open(INDEX_PATH) as template_file:
template_html = template_file.read()
template_html = template_html.replace('{{ app_url }}', app_url)
template_html = template_html.replace('{{ auth }}', auth)
handler.wfile.write(template_html.encode("UTF-8"))
def _handle_get_static(handler, path_match, data):
""" Returns a static file for the frontend. """
req_file = util.sanitize_path(path_match.group('file'))
# Strip md5 hash out of frontend filename
if re.match(r'^frontend-[A-Za-z0-9]{32}\.html$', req_file):
req_file = "frontend.html"
path = os.path.join(os.path.dirname(__file__), 'www_static', req_file)
handler.write_file(path)

View File

@@ -0,0 +1,50 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Home Assistant</title>
<link rel='manifest' href='/static/manifest.json' />
<link rel='shortcut icon' href='/static/favicon.ico' />
<link rel='icon' type='image/png'
href='/static/favicon-192x192.png' sizes='192x192'>
<link rel='apple-touch-icon' sizes='180x180'
href='/static/favicon-apple-180x180.png'>
<meta name='apple-mobile-web-app-capable' content='yes'>
<meta name='mobile-web-app-capable' content='yes'>
<meta name='viewport' content='width=device-width,
user-scalable=no' />
<meta name='theme-color' content='#03a9f4'>
<style>
#init {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
-webkit-justify-content: center;
-webkit-align-items: center;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
font-family: 'Roboto', 'Noto', sans-serif;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
#init p {
margin-bottom: 101px;
}
</style>
</head>
<body fullbleed>
<div id='init'>
<img src='/static/splash.png' height='230' />
<p>Initializing</p>
</div>
<script src='/static/webcomponents-lite.min.js'></script>
<link rel='import' href='/static/{{ app_url }}' />
<home-assistant auth='{{ auth }}'></home-assistant>
</body>
</html>

View File

@@ -0,0 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "e9060d58fc9034468cfefa9794026d0c"

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -0,0 +1,14 @@
{
"name": "Home Assistant",
"short_name": "Assistant",
"start_url": "/",
"display": "standalone",
"icons": [
{
"src": "\/static\/favicon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,59 +1,63 @@
"""
homeassistant.components.groups
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
homeassistant.components.group
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to group devices that can be turned on or off.
"""
import logging
import homeassistant as ha
import homeassistant.core as ha
from homeassistant.helpers import generate_entity_id
from homeassistant.helpers.event import track_state_change
from homeassistant.helpers.entity import Entity
import homeassistant.util as util
from homeassistant.components import (STATE_ON, STATE_OFF,
STATE_HOME, STATE_NOT_HOME,
ATTR_ENTITY_ID)
from homeassistant.const import (
ATTR_ENTITY_ID, STATE_ON, STATE_OFF,
STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN)
DOMAIN = "group"
DEPENDENCIES = []
ENTITY_ID_FORMAT = DOMAIN + ".{}"
_GROUP_TYPES = {
"on_off": (STATE_ON, STATE_OFF),
"home_not_home": (STATE_HOME, STATE_NOT_HOME)
}
ATTR_AUTO = "auto"
# List of ON/OFF state tuples for groupable states
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)]
def _get_group_type(state):
""" Determine the group type based on the given group type. """
for group_type, states in _GROUP_TYPES.items():
def _get_group_on_off(state):
""" Determine the group on/off states based on a state. """
for states in _GROUP_TYPES:
if state in states:
return group_type
return states
return None
return None, None
def is_on(statemachine, entity_id):
def is_on(hass, entity_id):
""" Returns if the group state is in its ON-state. """
state = statemachine.get_state(entity_id)
state = hass.states.get(entity_id)
if state:
group_type = _get_group_type(state.state)
group_on, _ = _get_group_on_off(state.state)
if group_type:
# We found group_type, compare to ON-state
return state.state == _GROUP_TYPES[group_type][0]
else:
return False
else:
return False
# If we found a group_type, compare to ON-state
return group_on is not None and state.state == group_on
return False
def expand_entity_ids(statemachine, entity_ids):
def expand_entity_ids(hass, entity_ids):
""" Returns the given list of entity ids and expands group ids into
the entity ids it represents if found. """
found_ids = []
for entity_id in entity_ids:
if not isinstance(entity_id, str):
continue
entity_id = entity_id.lower()
try:
# If entity_id points at a group, expand it
domain, _ = util.split_entity_id(entity_id)
@@ -61,7 +65,7 @@ def expand_entity_ids(statemachine, entity_ids):
if domain == DOMAIN:
found_ids.extend(
ent_id for ent_id
in get_entity_ids(statemachine, entity_id)
in get_entity_ids(hass, entity_id)
if ent_id not in found_ids)
else:
@@ -75,97 +79,149 @@ def expand_entity_ids(statemachine, entity_ids):
return found_ids
def get_entity_ids(statemachine, entity_id):
def get_entity_ids(hass, entity_id, domain_filter=None):
""" Get the entity ids that make up this group. """
entity_id = entity_id.lower()
try:
return \
statemachine.get_state(entity_id).attributes[ATTR_ENTITY_ID]
entity_ids = hass.states.get(entity_id).attributes[ATTR_ENTITY_ID]
if domain_filter:
domain_filter = domain_filter.lower()
return [ent_id for ent_id in entity_ids
if ent_id.startswith(domain_filter)]
else:
return entity_ids
except (AttributeError, KeyError):
# AttributeError if state did not exist
# KeyError if key did not exist in attributes
return []
# pylint: disable=too-many-branches, too-many-locals
def setup(bus, statemachine, name, entity_ids):
def setup(hass, config):
""" Sets up all groups found definded in the configuration. """
for name, entity_ids in config.get(DOMAIN, {}).items():
if isinstance(entity_ids, str):
entity_ids = entity_ids.split(",")
setup_group(hass, name, entity_ids)
return True
class Group(Entity):
""" Tracks a group of entity ids. """
# pylint: disable=too-many-instance-attributes
def __init__(self, hass, name, entity_ids=None, user_defined=True):
self.hass = hass
self._name = name
self._state = STATE_UNKNOWN
self.user_defined = user_defined
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=hass)
self.tracking = []
self.group_on = None
self.group_off = None
if entity_ids is not None:
self.update_tracked_entity_ids(entity_ids)
else:
self.update_ha_state(True)
@property
def should_poll(self):
return False
@property
def name(self):
return self._name
@property
def state(self):
return self._state
@property
def state_attributes(self):
return {
ATTR_ENTITY_ID: self.tracking,
ATTR_AUTO: not self.user_defined,
}
def update_tracked_entity_ids(self, entity_ids):
""" Update the tracked entity IDs. """
self.stop()
self.tracking = tuple(ent_id.lower() for ent_id in entity_ids)
self.group_on, self.group_off = None, None
self.update_ha_state(True)
self.start()
def start(self):
""" Starts the tracking. """
track_state_change(
self.hass, self.tracking, self._state_changed_listener)
def stop(self):
""" Unregisters the group from Home Assistant. """
self.hass.states.remove(self.entity_id)
self.hass.bus.remove_listener(
ha.EVENT_STATE_CHANGED, self._state_changed_listener)
def update(self):
""" Query all the tracked states and determine current group state. """
self._state = STATE_UNKNOWN
for entity_id in self.tracking:
state = self.hass.states.get(entity_id)
if state is not None:
self._process_tracked_state(state)
def _state_changed_listener(self, entity_id, old_state, new_state):
""" Listener to receive state changes of tracked entities. """
self._process_tracked_state(new_state)
self.update_ha_state()
def _process_tracked_state(self, tr_state):
""" Updates group state based on a new state of a tracked entity. """
# We have not determined type of group yet
if self.group_on is None:
self.group_on, self.group_off = _get_group_on_off(tr_state.state)
if self.group_on is not None:
# New state of the group is going to be based on the first
# state that we can recognize
self._state = tr_state.state
return
# There is already a group state
cur_gr_state = self._state
group_on, group_off = self.group_on, self.group_off
# if cur_gr_state = OFF and tr_state = ON: set ON
# if cur_gr_state = ON and tr_state = OFF: research
# else: ignore
if cur_gr_state == group_off and tr_state.state == group_on:
self._state = group_on
elif cur_gr_state == group_on and tr_state.state == group_off:
# Set to off if no other states are on
if not any(self.hass.states.is_state(ent_id, group_on)
for ent_id in self.tracking
if tr_state.entity_id != ent_id):
self._state = group_off
def setup_group(hass, name, entity_ids, user_defined=True):
""" Sets up a group state that is the combined state of
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
logger = logging.getLogger(__name__)
# Loop over the given entities to:
# - determine which group type this is (on_off, device_home)
# - if all states exist and have valid states
# - retrieve the current state of the group
errors = []
group_type, group_on, group_off, group_state = None, None, None, None
for entity_id in entity_ids:
state = statemachine.get_state(entity_id)
# Try to determine group type if we didn't yet
if not group_type and state:
group_type = _get_group_type(state.state)
if group_type:
group_on, group_off = _GROUP_TYPES[group_type]
group_state = group_off
else:
# We did not find a matching group_type
errors.append("Found unexpected state '{}'".format(
name, state.state))
break
# Check if entity exists
if not state:
errors.append("Entity {} does not exist".format(entity_id))
# Check if entity is valid state
elif state.state != group_off and state.state != group_on:
errors.append("State of {} is {} (expected: {}, {})".format(
entity_id, state.state, group_off, group_on))
# Keep track of the group state to init later on
elif group_state == group_off and state.state == group_on:
group_state = group_on
if errors:
logger.error("Error setting up state group {}: {}".format(
name, ", ".join(errors)))
return False
group_entity_id = ENTITY_ID_FORMAT.format(name)
state_attr = {ATTR_ENTITY_ID: entity_ids}
# pylint: disable=unused-argument
def update_group_state(entity_id, old_state, new_state):
""" Updates the group state based on a state change by a tracked
entity. """
cur_group_state = statemachine.get_state(group_entity_id).state
# if cur_group_state = OFF and new_state = ON: set ON
# if cur_group_state = ON and new_state = OFF: research
# else: ignore
if cur_group_state == group_off and new_state.state == group_on:
statemachine.set_state(group_entity_id, group_on, state_attr)
elif cur_group_state == group_on and new_state.state == group_off:
# Check if any of the other states is still on
if not any([statemachine.is_state(ent_id, group_on)
for ent_id in entity_ids if entity_id != ent_id]):
statemachine.set_state(group_entity_id, group_off, state_attr)
for entity_id in entity_ids:
ha.track_state_change(bus, entity_id, update_group_state)
statemachine.set_state(group_entity_id, group_state, state_attr)
return True
return Group(hass, name, entity_ids, user_defined)

View File

@@ -0,0 +1,155 @@
"""
homeassistant.components.history
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provide pre-made queries on top of the recorder component.
"""
import re
from datetime import timedelta
from itertools import groupby
from collections import defaultdict
import homeassistant.util.dt as dt_util
import homeassistant.components.recorder as recorder
from homeassistant.const import HTTP_BAD_REQUEST
DOMAIN = 'history'
DEPENDENCIES = ['recorder', 'http']
URL_HISTORY_PERIOD = re.compile(
r'/api/history/period(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
def last_5_states(entity_id):
""" Return the last 5 states for entity_id. """
entity_id = entity_id.lower()
query = """
SELECT * FROM states WHERE entity_id=? AND
last_changed=last_updated
ORDER BY state_id DESC LIMIT 0, 5
"""
return recorder.query_states(query, (entity_id, ))
def state_changes_during_period(start_time, end_time=None, entity_id=None):
"""
Return states changes during UTC period start_time - end_time.
"""
where = "last_changed=last_updated AND last_changed > ? "
data = [start_time]
if end_time is not None:
where += "AND last_changed < ? "
data.append(end_time)
if entity_id is not None:
where += "AND entity_id = ? "
data.append(entity_id.lower())
query = ("SELECT * FROM states WHERE {} "
"ORDER BY entity_id, last_changed ASC").format(where)
states = recorder.query_states(query, data)
result = defaultdict(list)
entity_ids = [entity_id] if entity_id is not None else None
# Get the states at the start time
for state in get_states(start_time, entity_ids):
state.last_changed = start_time
result[state.entity_id].append(state)
# Append all changes to it
for entity_id, group in groupby(states, lambda state: state.entity_id):
result[entity_id].extend(group)
return result
def get_states(utc_point_in_time, entity_ids=None, run=None):
""" Returns the states at a specific point in time. """
if run is None:
run = recorder.run_information(utc_point_in_time)
# History did not run before utc_point_in_time
if run is None:
return []
where = run.where_after_start_run + "AND created < ? "
where_data = [utc_point_in_time]
if entity_ids is not None:
where += "AND entity_id IN ({}) ".format(
",".join(['?'] * len(entity_ids)))
where_data.extend(entity_ids)
query = """
SELECT * FROM states
INNER JOIN (
SELECT max(state_id) AS max_state_id
FROM states WHERE {}
GROUP BY entity_id)
WHERE state_id = max_state_id
""".format(where)
return recorder.query_states(query, where_data)
def get_state(utc_point_in_time, entity_id, run=None):
""" Return a state at a specific point in time. """
states = get_states(utc_point_in_time, (entity_id,), run)
return states[0] if states else None
# pylint: disable=unused-argument
def setup(hass, config):
""" Setup history hooks. """
hass.http.register_path(
'GET',
re.compile(
r'/api/history/entity/(?P<entity_id>[a-zA-Z\._0-9]+)/'
r'recent_states'),
_api_last_5_states)
hass.http.register_path('GET', URL_HISTORY_PERIOD, _api_history_period)
return True
# pylint: disable=unused-argument
# pylint: disable=invalid-name
def _api_last_5_states(handler, path_match, data):
""" Return the last 5 states for an entity id as JSON. """
entity_id = path_match.group('entity_id')
handler.write_json(last_5_states(entity_id))
def _api_history_period(handler, path_match, data):
""" Return history over a period of time. """
date_str = path_match.group('date')
one_day = timedelta(seconds=86400)
if date_str:
start_date = dt_util.date_str_to_date(date_str)
if start_date is None:
handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST)
return
start_time = dt_util.as_utc(dt_util.start_of_local_day(start_date))
else:
start_time = dt_util.utcnow() - one_day
end_time = start_time + one_day
print("Fetchign", start_time, end_time)
entity_id = data.get('filter_entity_id')
handler.write_json(
state_changes_during_period(start_time, end_time, entity_id).values())

View File

@@ -0,0 +1,529 @@
"""
homeassistant.components.httpinterface
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module provides an API and a HTTP interface for debug purposes.
By default it will run on port 8123.
All API calls have to be accompanied by an 'api_password' parameter and will
return JSON. If successful calls will return status code 200 or 201.
Other status codes that can occur are:
- 400 (Bad Request)
- 401 (Unauthorized)
- 404 (Not Found)
- 405 (Method not allowed)
The api supports the following actions:
/api - GET
Returns message if API is up and running.
Example result:
{
"message": "API running."
}
/api/states - GET
Returns a list of entities for which a state is available
Example result:
[
{ .. state object .. },
{ .. state object .. }
]
/api/states/<entity_id> - GET
Returns the current state from an entity
Example result:
{
"attributes": {
"next_rising": "07:04:15 29-10-2013",
"next_setting": "18:00:31 29-10-2013"
},
"entity_id": "weather.sun",
"last_changed": "23:24:33 28-10-2013",
"state": "below_horizon"
}
/api/states/<entity_id> - POST
Updates the current state of an entity. Returns status code 201 if successful
with location header of updated resource and as body the new state.
parameter: new_state - string
optional parameter: attributes - JSON encoded object
Example result:
{
"attributes": {
"next_rising": "07:04:15 29-10-2013",
"next_setting": "18:00:31 29-10-2013"
},
"entity_id": "weather.sun",
"last_changed": "23:24:33 28-10-2013",
"state": "below_horizon"
}
/api/events/<event_type> - POST
Fires an event with event_type
optional parameter: event_data - JSON encoded object
Example result:
{
"message": "Event download_file fired."
}
"""
import json
import threading
import logging
import time
import gzip
import os
import random
import string
from datetime import timedelta
from homeassistant.util import Throttle
from http.server import SimpleHTTPRequestHandler, HTTPServer
from http import cookies
from socketserver import ThreadingMixIn
from urllib.parse import urlparse, parse_qs
import homeassistant.core as ha
from homeassistant.const import (
SERVER_PORT, CONTENT_TYPE_JSON,
HTTP_HEADER_HA_AUTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_ACCEPT_ENCODING,
HTTP_HEADER_CONTENT_ENCODING, HTTP_HEADER_VARY, HTTP_HEADER_CONTENT_LENGTH,
HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_EXPIRES, HTTP_OK, HTTP_UNAUTHORIZED,
HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED, HTTP_UNPROCESSABLE_ENTITY)
import homeassistant.remote as rem
import homeassistant.util as util
import homeassistant.util.dt as date_util
import homeassistant.bootstrap as bootstrap
DOMAIN = "http"
DEPENDENCIES = []
CONF_API_PASSWORD = "api_password"
CONF_SERVER_HOST = "server_host"
CONF_SERVER_PORT = "server_port"
CONF_DEVELOPMENT = "development"
CONF_SESSIONS_ENABLED = "sessions_enabled"
DATA_API_PASSWORD = 'api_password'
# Throttling time in seconds for expired sessions check
MIN_SEC_SESSION_CLEARING = timedelta(seconds=20)
SESSION_TIMEOUT_SECONDS = 1800
SESSION_KEY = 'sessionId'
_LOGGER = logging.getLogger(__name__)
def setup(hass, config=None):
""" Sets up the HTTP API and debug interface. """
if config is None or DOMAIN not in config:
config = {DOMAIN: {}}
api_password = util.convert(config[DOMAIN].get(CONF_API_PASSWORD), str)
no_password_set = api_password is None
if no_password_set:
api_password = util.get_random_string()
# If no server host is given, accept all incoming requests
server_host = config[DOMAIN].get(CONF_SERVER_HOST, '0.0.0.0')
server_port = config[DOMAIN].get(CONF_SERVER_PORT, SERVER_PORT)
development = str(config[DOMAIN].get(CONF_DEVELOPMENT, "")) == "1"
sessions_enabled = config[DOMAIN].get(CONF_SESSIONS_ENABLED, True)
try:
server = HomeAssistantHTTPServer(
(server_host, server_port), RequestHandler, hass, api_password,
development, no_password_set, sessions_enabled)
except OSError:
# Happens if address already in use
_LOGGER.exception("Error setting up HTTP server")
return False
hass.bus.listen_once(
ha.EVENT_HOMEASSISTANT_START,
lambda event:
threading.Thread(target=server.start, daemon=True).start())
hass.http = server
hass.config.api = rem.API(util.get_local_ip(), api_password, server_port)
return True
# pylint: disable=too-many-instance-attributes
class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
""" Handle HTTP requests in a threaded fashion. """
# pylint: disable=too-few-public-methods
allow_reuse_address = True
daemon_threads = True
# pylint: disable=too-many-arguments
def __init__(self, server_address, request_handler_class,
hass, api_password, development, no_password_set,
sessions_enabled):
super().__init__(server_address, request_handler_class)
self.server_address = server_address
self.hass = hass
self.api_password = api_password
self.development = development
self.no_password_set = no_password_set
self.paths = []
self.sessions = SessionStore(sessions_enabled)
# We will lazy init this one if needed
self.event_forwarder = None
if development:
_LOGGER.info("running http in development mode")
def start(self):
""" Starts the HTTP server. """
def stop_http(event):
""" Stops the HTTP server. """
self.shutdown()
self.hass.bus.listen_once(ha.EVENT_HOMEASSISTANT_STOP, stop_http)
_LOGGER.info(
"Starting web interface at http://%s:%d", *self.server_address)
# 31-1-2015: Refactored frontend/api components out of this component
# To prevent stuff from breaking, load the two extracted components
bootstrap.setup_component(self.hass, 'api')
bootstrap.setup_component(self.hass, 'frontend')
self.serve_forever()
def register_path(self, method, url, callback, require_auth=True):
""" Registers a path wit the server. """
self.paths.append((method, url, callback, require_auth))
# pylint: disable=too-many-public-methods,too-many-locals
class RequestHandler(SimpleHTTPRequestHandler):
"""
Handles incoming HTTP requests
We extend from SimpleHTTPRequestHandler instead of Base so we
can use the guess content type methods.
"""
server_version = "HomeAssistant/1.0"
def __init__(self, req, client_addr, server):
""" Contructor, call the base constructor and set up session """
self._session = None
SimpleHTTPRequestHandler.__init__(self, req, client_addr, server)
def _handle_request(self, method): # pylint: disable=too-many-branches
""" Does some common checks and calls appropriate method. """
url = urlparse(self.path)
# Read query input
data = parse_qs(url.query)
# parse_qs gives a list for each value, take the latest element
for key in data:
data[key] = data[key][-1]
# Did we get post input ?
content_length = int(self.headers.get(HTTP_HEADER_CONTENT_LENGTH, 0))
if content_length:
body_content = self.rfile.read(content_length).decode("UTF-8")
try:
data.update(json.loads(body_content))
except (TypeError, ValueError):
# TypeError if JSON object is not a dict
# ValueError if we could not parse JSON
_LOGGER.exception(
"Exception parsing JSON: %s", body_content)
self.write_json_message(
"Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
return
self._session = self.get_session()
if self.server.no_password_set:
api_password = self.server.api_password
else:
api_password = self.headers.get(HTTP_HEADER_HA_AUTH)
if not api_password and DATA_API_PASSWORD in data:
api_password = data[DATA_API_PASSWORD]
if not api_password and self._session is not None:
api_password = self._session.cookie_values.get(
CONF_API_PASSWORD)
if '_METHOD' in data:
method = data.pop('_METHOD')
# Var to keep track if we found a path that matched a handler but
# the method was different
path_matched_but_not_method = False
# Var to hold the handler for this path and method if found
handle_request_method = False
require_auth = True
# Check every handler to find matching result
for t_method, t_path, t_handler, t_auth in self.server.paths:
# we either do string-comparison or regular expression matching
# pylint: disable=maybe-no-member
if isinstance(t_path, str):
path_match = url.path == t_path
else:
path_match = t_path.match(url.path)
if path_match and method == t_method:
# Call the method
handle_request_method = t_handler
require_auth = t_auth
break
elif path_match:
path_matched_but_not_method = True
# Did we find a handler for the incoming request?
if handle_request_method:
# For some calls we need a valid password
if require_auth and api_password != self.server.api_password:
self.write_json_message(
"API password missing or incorrect.", HTTP_UNAUTHORIZED)
else:
if self._session is None and require_auth:
self._session = self.server.sessions.create(
api_password)
handle_request_method(self, path_match, data)
elif path_matched_but_not_method:
self.send_response(HTTP_METHOD_NOT_ALLOWED)
self.end_headers()
else:
self.send_response(HTTP_NOT_FOUND)
self.end_headers()
def do_HEAD(self): # pylint: disable=invalid-name
""" HEAD request handler. """
self._handle_request('HEAD')
def do_GET(self): # pylint: disable=invalid-name
""" GET request handler. """
self._handle_request('GET')
def do_POST(self): # pylint: disable=invalid-name
""" POST request handler. """
self._handle_request('POST')
def do_PUT(self): # pylint: disable=invalid-name
""" PUT request handler. """
self._handle_request('PUT')
def do_DELETE(self): # pylint: disable=invalid-name
""" DELETE request handler. """
self._handle_request('DELETE')
def write_json_message(self, message, status_code=HTTP_OK):
""" Helper method to return a message to the caller. """
self.write_json({'message': message}, status_code=status_code)
def write_json(self, data=None, status_code=HTTP_OK, location=None):
""" Helper method to return JSON to the caller. """
self.send_response(status_code)
self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON)
if location:
self.send_header('Location', location)
self.set_session_cookie_header()
self.end_headers()
if data is not None:
self.wfile.write(
json.dumps(data, indent=4, sort_keys=True,
cls=rem.JSONEncoder).encode("UTF-8"))
def write_file(self, path):
""" Returns a file to the user. """
try:
with open(path, 'rb') as inp:
self.write_file_pointer(self.guess_type(path), inp)
except IOError:
self.send_response(HTTP_NOT_FOUND)
self.end_headers()
_LOGGER.exception("Unable to serve %s", path)
def write_file_pointer(self, content_type, inp):
"""
Helper function to write a file pointer to the user.
Does not do error handling.
"""
do_gzip = 'gzip' in self.headers.get(HTTP_HEADER_ACCEPT_ENCODING, '')
self.send_response(HTTP_OK)
self.send_header(HTTP_HEADER_CONTENT_TYPE, content_type)
self.set_cache_header()
self.set_session_cookie_header()
if do_gzip:
gzip_data = gzip.compress(inp.read())
self.send_header(HTTP_HEADER_CONTENT_ENCODING, "gzip")
self.send_header(HTTP_HEADER_VARY, HTTP_HEADER_ACCEPT_ENCODING)
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(gzip_data)))
else:
fst = os.fstat(inp.fileno())
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(fst[6]))
self.end_headers()
if self.command == 'HEAD':
return
elif do_gzip:
self.wfile.write(gzip_data)
else:
self.copyfile(inp, self.wfile)
def set_cache_header(self):
""" Add cache headers if not in development """
if not self.server.development:
# 1 year in seconds
cache_time = 365 * 86400
self.send_header(
HTTP_HEADER_CACHE_CONTROL,
"public, max-age={}".format(cache_time))
self.send_header(
HTTP_HEADER_EXPIRES,
self.date_time_string(time.time()+cache_time))
def set_session_cookie_header(self):
""" Add the header for the session cookie """
if self.server.sessions.enabled and self._session is not None:
existing_sess_id = self.get_current_session_id()
if existing_sess_id != self._session.session_id:
self.send_header(
'Set-Cookie',
SESSION_KEY+'='+self._session.session_id)
def get_session(self):
""" Get the requested session object from cookie value """
if self.server.sessions.enabled is not True:
return None
session_id = self.get_current_session_id()
if session_id is not None:
session = self.server.sessions.get(session_id)
if session is not None:
session.reset_expiry()
return session
return None
def get_current_session_id(self):
"""
Extracts the current session id from the
cookie or returns None if not set
"""
cookie = cookies.SimpleCookie()
if self.headers.get('Cookie', None) is not None:
cookie.load(self.headers.get("Cookie"))
if cookie.get(SESSION_KEY, False):
return cookie[SESSION_KEY].value
return None
class ServerSession:
""" A very simple session class """
def __init__(self, session_id):
""" Set up the expiry time on creation """
self._expiry = 0
self.reset_expiry()
self.cookie_values = {}
self.session_id = session_id
def reset_expiry(self):
""" Resets the expiry based on current time """
self._expiry = date_util.utcnow() + timedelta(
seconds=SESSION_TIMEOUT_SECONDS)
@property
def is_expired(self):
""" Return true if the session is expired based on the expiry time """
return self._expiry < date_util.utcnow()
class SessionStore:
""" Responsible for storing and retrieving http sessions """
def __init__(self, enabled=True):
""" Set up the session store """
self._sessions = {}
self.enabled = enabled
self.session_lock = threading.RLock()
@Throttle(MIN_SEC_SESSION_CLEARING)
def remove_expired(self):
""" Remove any expired sessions. """
if self.session_lock.acquire(False):
try:
keys = []
for key in self._sessions.keys():
keys.append(key)
for key in keys:
if self._sessions[key].is_expired:
del self._sessions[key]
_LOGGER.info("Cleared expired session %s", key)
finally:
self.session_lock.release()
def add(self, key, session):
""" Add a new session to the list of tracked sessions """
self.remove_expired()
with self.session_lock:
self._sessions[key] = session
def get(self, key):
""" get a session by key """
self.remove_expired()
session = self._sessions.get(key, None)
if session is not None and session.is_expired:
return None
return session
def create(self, api_password):
""" Creates a new session and adds it to the sessions """
if self.enabled is not True:
return None
chars = string.ascii_letters + string.digits
session_id = ''.join([random.choice(chars) for i in range(20)])
session = ServerSession(session_id)
session.cookie_values[CONF_API_PASSWORD] = api_password
self.add(session_id, session)
return session

View File

@@ -1,689 +0,0 @@
"""
homeassistant.components.httpinterface
~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module provides an API and a HTTP interface for debug purposes.
By default it will run on port 8123.
All API calls have to be accompanied by an 'api_password' parameter and will
return JSON. If successful calls will return status code 200 or 201.
Other status codes that can occur are:
- 400 (Bad Request)
- 401 (Unauthorized)
- 404 (Not Found)
- 405 (Method not allowed)
The api supports the following actions:
/api/states - GET
Returns a list of entities for which a state is available
Example result:
{
"entity_ids": [
"Paulus_Nexus_4",
"weather.sun",
"all_devices"
]
}
/api/states/<entity_id> - GET
Returns the current state from an entity
Example result:
{
"attributes": {
"next_rising": "07:04:15 29-10-2013",
"next_setting": "18:00:31 29-10-2013"
},
"entity_id": "weather.sun",
"last_changed": "23:24:33 28-10-2013",
"state": "below_horizon"
}
/api/states/<entity_id> - POST
Updates the current state of an entity. Returns status code 201 if successful
with location header of updated resource and as body the new state.
parameter: new_state - string
optional parameter: attributes - JSON encoded object
Example result:
{
"attributes": {
"next_rising": "07:04:15 29-10-2013",
"next_setting": "18:00:31 29-10-2013"
},
"entity_id": "weather.sun",
"last_changed": "23:24:33 28-10-2013",
"state": "below_horizon"
}
/api/events/<event_type> - POST
Fires an event with event_type
optional parameter: event_data - JSON encoded object
Example result:
{
"message": "Event download_file fired."
}
"""
import json
import threading
import logging
import re
import os
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from urlparse import urlparse, parse_qs
import homeassistant as ha
import homeassistant.util as util
SERVER_PORT = 8123
HTTP_OK = 200
HTTP_CREATED = 201
HTTP_MOVED_PERMANENTLY = 301
HTTP_BAD_REQUEST = 400
HTTP_UNAUTHORIZED = 401
HTTP_NOT_FOUND = 404
HTTP_METHOD_NOT_ALLOWED = 405
HTTP_UNPROCESSABLE_ENTITY = 422
URL_ROOT = "/"
URL_CHANGE_STATE = "/change_state"
URL_FIRE_EVENT = "/fire_event"
URL_API_STATES = "/api/states"
URL_API_STATES_ENTITY = "/api/states/{}"
URL_API_EVENTS = "/api/events"
URL_API_EVENTS_EVENT = "/api/events/{}"
URL_API_SERVICES = "/api/services"
URL_API_SERVICES_SERVICE = "/api/services/{}/{}"
URL_STATIC = "/static/{}"
class HTTPInterface(threading.Thread):
""" Provides an HTTP interface for Home Assistant. """
# pylint: disable=too-many-arguments
def __init__(self, bus, statemachine, api_password,
server_port=None, server_host=None):
threading.Thread.__init__(self)
self.daemon = True
if not server_port:
server_port = SERVER_PORT
# If no server host is given, accept all incoming requests
if not server_host:
server_host = '0.0.0.0'
self.server = HTTPServer((server_host, server_port), RequestHandler)
self.server.flash_message = None
self.server.logger = logging.getLogger(__name__)
self.server.bus = bus
self.server.statemachine = statemachine
self.server.api_password = api_password
bus.listen_once_event(ha.EVENT_HOMEASSISTANT_START,
lambda event: self.start())
def run(self):
""" Start the HTTP interface. """
self.server.logger.info("Starting")
self.server.serve_forever()
class RequestHandler(BaseHTTPRequestHandler):
""" Handles incoming HTTP requests """
PATHS = [ # debug interface
('GET', '/', '_handle_get_root'),
('POST', re.compile(r'/change_state'), '_handle_change_state'),
('POST', re.compile(r'/fire_event'), '_handle_fire_event'),
('POST', re.compile(r'/call_service'), '_handle_call_service'),
# /states
('GET', '/api/states', '_handle_get_api_states'),
('GET',
re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
'_handle_get_api_states_entity'),
('POST',
re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
'_handle_change_state'),
# /events
('GET', '/api/events', '_handle_get_api_events'),
('POST',
re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
'_handle_fire_event'),
# /services
('GET', '/api/services', '_handle_get_api_services'),
('POST',
re.compile((r'/api/services/'
r'(?P<domain>[a-zA-Z\._0-9]+)/'
r'(?P<service>[a-zA-Z\._0-9]+)')),
'_handle_call_service'),
# Statis files
('GET', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
'_handle_get_static')
]
use_json = False
def _handle_request(self, method): # pylint: disable=too-many-branches
""" Does some common checks and calls appropriate method. """
url = urlparse(self.path)
# Read query input
data = parse_qs(url.query)
# Did we get post input ?
content_length = int(self.headers.get('Content-Length', 0))
if content_length:
data.update(parse_qs(self.rfile.read(content_length)))
try:
api_password = data['api_password'][0]
except KeyError:
api_password = ''
if url.path.startswith('/api/'):
self.use_json = True
# Var to keep track if we found a path that matched a handler but
# the method was different
path_matched_but_not_method = False
# Var to hold the handler for this path and method if found
handle_request_method = False
# Check every handler to find matching result
for t_method, t_path, t_handler in RequestHandler.PATHS:
# we either do string-comparison or regular expression matching
# pylint: disable=maybe-no-member
if isinstance(t_path, str):
path_match = url.path == t_path
else:
path_match = t_path.match(url.path)
if path_match and method == t_method:
# Call the method
handle_request_method = getattr(self, t_handler)
break
elif path_match:
path_matched_but_not_method = True
# Did we find a handler for the incoming request?
if handle_request_method:
# Do not enforce api password for static files
if handle_request_method == self._handle_get_static or \
self._verify_api_password(api_password):
handle_request_method(path_match, data)
elif path_matched_but_not_method:
self.send_response(HTTP_METHOD_NOT_ALLOWED)
else:
self.send_response(HTTP_NOT_FOUND)
def do_GET(self): # pylint: disable=invalid-name
""" GET request handler. """
self._handle_request('GET')
def do_POST(self): # pylint: disable=invalid-name
""" POST request handler. """
self._handle_request('POST')
def _verify_api_password(self, api_password):
""" Helper method to verify the API password
and take action if incorrect. """
if api_password == self.server.api_password:
return True
elif self.use_json:
self._message(
"API password missing or incorrect.", HTTP_UNAUTHORIZED)
else:
self.send_response(HTTP_OK)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write((
"<html>"
"<head><title>Home Assistant</title>"
"<link rel='stylesheet' type='text/css' "
" href='/static/style.css'>"
"<link rel='icon' href='/static/favicon.ico' "
" type='image/x-icon' />"
"</head>"
"<body>"
"<div class='container'>"
"<form class='form-signin' action='{}' method='GET'>"
"<input type='text' class='form-control' name='api_password' "
" placeholder='API Password for Home Assistant' "
" required autofocus>"
"<button class='btn btn-lg btn-primary btn-block' "
" type='submit'>Enter</button>"
"</form>"
"</div>"
"</body></html>").format(self.path))
return False
# pylint: disable=unused-argument
def _handle_get_root(self, path_match, data):
""" Renders the debug interface. """
write = lambda txt: self.wfile.write(txt.encode("UTF-8")+"\n")
self.send_response(HTTP_OK)
self.send_header('Content-type', 'text/html; charset=utf-8')
self.end_headers()
write(("<html>"
"<head><title>Home Assistant</title>"
"<link rel='stylesheet' type='text/css' "
" href='/static/style.css'>"
"<link rel='icon' href='/static/favicon.ico' "
" type='image/x-icon' />"
"</head>"
"<body>"
"<div class='container'>"
"<div class='page-header'><h1>Home Assistant</h1></div>"))
# Flash message support
if self.server.flash_message:
write(("<div class='row'><div class='col-xs-12'>"
"<div class='alert alert-success'>"
"{}</div></div></div>").format(self.server.flash_message))
self.server.flash_message = None
# Describe state machine:
write(("<div class='row'>"
"<div class='col-xs-12'>"
"<div class='panel panel-primary'>"
"<div class='panel-heading'><h2 class='panel-title'>"
" States</h2></div>"
"<form method='post' action='/change_state' "
" class='form-change-state'>"
"<input type='hidden' name='api_password' value='{}'>"
"<table class='table'><tr>"
"<th>Entity ID</th><th>State</th>"
"<th>Attributes</th><th>Last Changed</th>"
"</tr>").format(self.server.api_password))
for entity_id in \
sorted(self.server.statemachine.entity_ids,
key=lambda key: key.lower()):
state = self.server.statemachine.get_state(entity_id)
attributes = u"<br>".join(
[u"{}: {}".format(attr, state.attributes[attr])
for attr in state.attributes])
write((u"<tr>"
u"<td>{}</td><td>{}</td><td>{}</td><td>{}</td>"
u"</tr>").format(
entity_id,
state.state,
attributes,
util.datetime_to_str(state.last_changed)))
# Change state form
write(("<tr><td><input name='entity_id' class='form-control' "
" placeholder='Entity ID'></td>"
"<td><input name='new_state' class='form-control' "
" placeholder='New State'></td>"
"<td><textarea rows='3' name='attributes' class='form-control' "
" placeholder='State Attributes (JSON, optional)'>"
"</textarea></td>"
"<td><button type='submit' class='btn btn-default'>"
"Set State</button></td></tr>"
"</table></form></div>"
"</div></div>"))
# Describe bus/services:
write(("<div class='row'>"
"<div class='col-xs-6'>"
"<div class='panel panel-primary'>"
"<div class='panel-heading'><h2 class='panel-title'>"
" Services</h2></div>"
"<table class='table'>"
"<tr><th>Domain</th><th>Service</th></tr>"))
for domain, services in sorted(
self.server.bus.services.items()):
write("<tr><td>{}</td><td>{}</td></tr>".format(
domain, ", ".join(services)))
write(("</table></div></div>"
"<div class='col-xs-6'>"
"<div class='panel panel-primary'>"
"<div class='panel-heading'><h2 class='panel-title'>"
" Call Service</h2></div>"
"<div class='panel-body'>"
"<form method='post' action='/call_service' "
" class='form-horizontal form-fire-event'>"
"<input type='hidden' name='api_password' value='{}'>"
"<div class='form-group'>"
" <label for='domain' class='col-xs-3 control-label'>"
" Domain</label>"
" <div class='col-xs-9'>"
" <input type='text' class='form-control' id='domain'"
" name='domain' placeholder='Service Domain'>"
" </div>"
"</div>"
"<div class='form-group'>"
" <label for='service' class='col-xs-3 control-label'>"
" Service</label>"
" <div class='col-xs-9'>"
" <input type='text' class='form-control' id='service'"
" name='service' placeholder='Service name'>"
" </div>"
"</div>"
"<div class='form-group'>"
" <label for='service_data' class='col-xs-3 control-label'>"
" Service data</label>"
" <div class='col-xs-9'>"
" <textarea rows='3' class='form-control' id='service_data'"
" name='service_data' placeholder='Service Data "
"(JSON, optional)'></textarea>"
" </div>"
"</div>"
"<div class='form-group'>"
" <div class='col-xs-offset-3 col-xs-9'>"
" <button type='submit' class='btn btn-default'>"
" Call Service</button>"
" </div>"
"</div>"
"</form>"
"</div></div></div>"
"</div>").format(self.server.api_password))
# Describe bus/events:
write(("<div class='row'>"
"<div class='col-xs-6'>"
"<div class='panel panel-primary'>"
"<div class='panel-heading'><h2 class='panel-title'>"
" Events</h2></div>"
"<table class='table'>"
"<tr><th>Event</th><th>Listeners</th></tr>"))
for event, listener_count in sorted(
self.server.bus.event_listeners.items()):
write("<tr><td>{}</td><td>{}</td></tr>".format(
event, listener_count))
write(("</table></div></div>"
"<div class='col-xs-6'>"
"<div class='panel panel-primary'>"
"<div class='panel-heading'><h2 class='panel-title'>"
" Fire Event</h2></div>"
"<div class='panel-body'>"
"<form method='post' action='/fire_event' "
" class='form-horizontal form-fire-event'>"
"<input type='hidden' name='api_password' value='{}'>"
"<div class='form-group'>"
" <label for='event_type' class='col-xs-3 control-label'>"
" Event type</label>"
" <div class='col-xs-9'>"
" <input type='text' class='form-control' id='event_type'"
" name='event_type' placeholder='Event Type'>"
" </div>"
"</div>"
"<div class='form-group'>"
" <label for='event_data' class='col-xs-3 control-label'>"
" Event data</label>"
" <div class='col-xs-9'>"
" <textarea rows='3' class='form-control' id='event_data'"
" name='event_data' placeholder='Event Data "
"(JSON, optional)'></textarea>"
" </div>"
"</div>"
"<div class='form-group'>"
" <div class='col-xs-offset-3 col-xs-9'>"
" <button type='submit' class='btn btn-default'>"
" Fire Event</button>"
" </div>"
"</div>"
"</form>"
"</div></div></div>"
"</div>").format(self.server.api_password))
write("</div></body></html>")
# pylint: disable=invalid-name
def _handle_change_state(self, path_match, data):
""" Handles updating the state of an entity.
This handles the following paths:
/change_state
/api/states/<entity_id>
"""
try:
try:
entity_id = path_match.group('entity_id')
except IndexError:
# If group 'entity_id' does not exist in path_match
entity_id = data['entity_id'][0]
new_state = data['new_state'][0]
try:
attributes = json.loads(data['attributes'][0])
except KeyError:
# Happens if key 'attributes' does not exist
attributes = None
# Write state
self.server.statemachine.set_state(entity_id,
new_state,
attributes)
# Return state if json, else redirect to main page
if self.use_json:
state = self.server.statemachine.get_state(entity_id)
self._write_json(state.as_dict(),
status_code=HTTP_CREATED,
location=
URL_API_STATES_ENTITY.format(entity_id))
else:
self._message(
"State of {} changed to {}".format(entity_id, new_state))
except KeyError:
# If new_state don't exist in post data
self._message(
"No new_state submitted.", HTTP_BAD_REQUEST)
except ValueError:
# Occurs during error parsing json
self._message(
"Invalid JSON for attributes", HTTP_UNPROCESSABLE_ENTITY)
# pylint: disable=invalid-name
def _handle_fire_event(self, path_match, data):
""" Handles firing of an event.
This handles the following paths:
/fire_event
/api/events/<event_type>
"""
try:
try:
event_type = path_match.group('event_type')
except IndexError:
# If group event_type does not exist in path_match
event_type = data['event_type'][0]
try:
event_data = json.loads(data['event_data'][0])
except KeyError:
# Happens if key 'event_data' does not exist
event_data = None
self.server.bus.fire_event(event_type, event_data)
self._message("Event {} fired.".format(event_type))
except KeyError:
# Occurs if event_type does not exist in data
self._message("No event_type received.", HTTP_BAD_REQUEST)
except ValueError:
# Occurs during error parsing json
self._message(
"Invalid JSON for event_data", HTTP_UNPROCESSABLE_ENTITY)
def _handle_call_service(self, path_match, data):
""" Handles calling a service.
This handles the following paths:
/call_service
/api/services/<domain>/<service>
"""
try:
try:
domain = path_match.group('domain')
service = path_match.group('service')
except IndexError:
# If group domain or service does not exist in path_match
domain = data['domain'][0]
service = data['service'][0]
try:
service_data = json.loads(data['service_data'][0])
except KeyError:
# Happens if key 'service_data' does not exist
service_data = None
self.server.bus.call_service(domain, service, service_data)
self._message("Service {}/{} called.".format(domain, service))
except ha.ServiceDoesNotExistError:
# If the service does not exist
self._message('Service does not exist', HTTP_BAD_REQUEST)
except KeyError:
# Occurs if domain or service does not exist in data
self._message("No domain or service received.", HTTP_BAD_REQUEST)
except ValueError:
# Occurs during error parsing json
self._message(
"Invalid JSON for service_data", HTTP_UNPROCESSABLE_ENTITY)
# pylint: disable=unused-argument
def _handle_get_api_states(self, path_match, data):
""" Returns the entitie ids which state are being tracked. """
self._write_json({'entity_ids': self.server.statemachine.entity_ids})
# pylint: disable=unused-argument
def _handle_get_api_states_entity(self, path_match, data):
""" Returns the state of a specific entity. """
entity_id = path_match.group('entity_id')
state = self.server.statemachine.get_state(entity_id)
try:
self._write_json(state.as_dict())
except AttributeError:
# If state for entity_id does not exist
self._message("State does not exist.", HTTP_UNPROCESSABLE_ENTITY)
def _handle_get_api_events(self, path_match, data):
""" Handles getting overview of event listeners. """
self._write_json({'event_listeners': self.server.bus.event_listeners})
def _handle_get_api_services(self, path_match, data):
""" Handles getting overview of services. """
self._write_json({'services': self.server.bus.services})
def _handle_get_static(self, path_match, data):
""" Returns a static file. """
req_file = util.sanitize_filename(path_match.group('file'))
path = os.path.join(os.path.dirname(__file__), 'www_static', req_file)
if os.path.isfile(path):
self.send_response(HTTP_OK)
# TODO: correct header for mime-type and caching
self.end_headers()
with open(path, 'rb') as inp:
data = inp.read(1024)
while data:
self.wfile.write(data)
data = inp.read(1024)
else:
self.send_response(HTTP_NOT_FOUND)
self.end_headers()
def _message(self, message, status_code=HTTP_OK):
""" Helper method to return a message to the caller. """
if self.use_json:
self._write_json({'message': message}, status_code=status_code)
elif status_code == HTTP_OK:
self.server.flash_message = message
self._redirect('/')
else:
self.send_error(status_code, message)
def _redirect(self, location):
""" Helper method to redirect caller. """
self.send_response(HTTP_MOVED_PERMANENTLY)
self.send_header(
"Location", "{}?api_password={}".format(
location, self.server.api_password))
self.end_headers()
def _write_json(self, data=None, status_code=HTTP_OK, location=None):
""" Helper method to return JSON to the caller. """
self.send_response(status_code)
self.send_header('Content-type', 'application/json')
if location:
self.send_header('Location', location)
self.end_headers()
if data:
self.wfile.write(json.dumps(data, indent=4, sort_keys=True))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 B

View File

@@ -1,39 +0,0 @@
@import url(//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.0.2/css/bootstrap.min.css);
.panel > form, .panel > form > .table {
margin-bottom: 0;
}
.panel .table {
font-size: inherit;
}
.form-signin {
max-width: 330px;
margin: 0 auto;
}
.form-signin .form-control {
margin-top: 40px;
position: relative;
font-size: 16px;
height: auto;
padding: 10px;
margin-bottom: -1px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
text-align: center;
}
.form-signin .btn-primary {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.form-fire-event {
margin-bottom: 0px;
}
.form-fire-event .form-group:last-child {
margin-bottom: 0;
}

View File

@@ -0,0 +1,41 @@
"""
homeassistant.components.introduction
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Component that will help guide the user taking its first steps.
"""
import logging
DOMAIN = 'introduction'
DEPENDENCIES = []
def setup(hass, config=None):
""" Setup the introduction component. """
log = logging.getLogger(__name__)
log.info("""
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Hello, and welcome to Home Assistant!
We'll hope that we can make all your dreams come true.
Here are some resources to get started:
- Configuring Home Assistant:
https://home-assistant.io/getting-started/configuration.html
- Available components:
https://home-assistant.io/components/
- Chat room:
https://gitter.im/balloob/home-assistant
This message is generated by the introduction component. You can
disable it in configuration.yaml.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
""")
return True

View File

@@ -0,0 +1,232 @@
"""
homeassistant.components.isy994
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Connects to an ISY-994 controller and loads relevant components to control its
devices. Also contains the base classes for ISY Sensors, Lights, and Switches.
For configuration details please visit the documentation for this component at
https://home-assistant.io/components/isy994.html
"""
import logging
from urllib.parse import urlparse
from homeassistant import bootstrap
from homeassistant.loader import get_component
from homeassistant.helpers import validate_config
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import (
CONF_HOST, CONF_USERNAME, CONF_PASSWORD, EVENT_PLATFORM_DISCOVERED,
EVENT_HOMEASSISTANT_STOP, ATTR_SERVICE, ATTR_DISCOVERED,
ATTR_FRIENDLY_NAME)
DOMAIN = "isy994"
DEPENDENCIES = []
REQUIREMENTS = ['PyISY==1.0.5']
DISCOVER_LIGHTS = "isy994.lights"
DISCOVER_SWITCHES = "isy994.switches"
DISCOVER_SENSORS = "isy994.sensors"
ISY = None
SENSOR_STRING = 'Sensor'
HIDDEN_STRING = '{HIDE ME}'
CONF_TLS_VER = 'tls'
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
"""
Setup ISY994 component.
This will automatically import associated lights, switches, and sensors.
"""
try:
import PyISY
except ImportError:
_LOGGER.error("Error while importing dependency PyISY.")
return False
# pylint: disable=global-statement
# check for required values in configuration file
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return False
# pull and parse standard configuration
user = config[DOMAIN][CONF_USERNAME]
password = config[DOMAIN][CONF_PASSWORD]
host = urlparse(config[DOMAIN][CONF_HOST])
addr = host.geturl()
if host.scheme == 'http':
addr = addr.replace('http://', '')
https = False
elif host.scheme == 'https':
addr = addr.replace('https://', '')
https = True
else:
_LOGGER.error('isy994 host value in configuration file is invalid.')
return False
port = host.port
addr = addr.replace(':{}'.format(port), '')
# pull and parse optional configuration
global SENSOR_STRING
global HIDDEN_STRING
SENSOR_STRING = str(config[DOMAIN].get('sensor_string', SENSOR_STRING))
HIDDEN_STRING = str(config[DOMAIN].get('hidden_string', HIDDEN_STRING))
tls_version = config[DOMAIN].get(CONF_TLS_VER, None)
# connect to ISY controller
global ISY
ISY = PyISY.ISY(addr, port, user, password, use_https=https,
tls_ver=tls_version, log=_LOGGER)
if not ISY.connected:
return False
# listen for HA stop to disconnect
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop)
# Load components for the devices in the ISY controller that we support
for comp_name, discovery in ((('sensor', DISCOVER_SENSORS),
('light', DISCOVER_LIGHTS),
('switch', DISCOVER_SWITCHES))):
component = get_component(comp_name)
bootstrap.setup_component(hass, component.DOMAIN, config)
hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
{ATTR_SERVICE: discovery,
ATTR_DISCOVERED: {}})
ISY.auto_update = True
return True
def stop(event):
""" Cleanup the ISY subscription. """
ISY.auto_update = False
class ISYDeviceABC(ToggleEntity):
""" Abstract Class for an ISY device. """
_attrs = {}
_onattrs = []
_states = []
_dtype = None
_domain = None
_name = None
def __init__(self, node):
# setup properties
self.node = node
self.hidden = HIDDEN_STRING in self.raw_name
# track changes
self._change_handler = self.node.status. \
subscribe('changed', self.on_update)
def __del__(self):
""" cleanup subscriptions because it is the right thing to do. """
self._change_handler.unsubscribe()
@property
def domain(self):
""" Returns the domain of the entity. """
return self._domain
@property
def dtype(self):
""" Returns the data type of the entity (binary or analog). """
if self._dtype in ['analog', 'binary']:
return self._dtype
return 'binary' if self.unit_of_measurement is None else 'analog'
@property
def should_poll(self):
""" Tells Home Assistant not to poll this entity. """
return False
@property
def value(self):
""" Returns the unclean value from the controller. """
# pylint: disable=protected-access
return self.node.status._val
@property
def state_attributes(self):
""" Returns the state attributes for the node. """
attr = {ATTR_FRIENDLY_NAME: self.name}
for name, prop in self._attrs.items():
attr[name] = getattr(self, prop)
attr = self._attr_filter(attr)
return attr
def _attr_filter(self, attr):
""" Placeholder for attribute filters. """
# pylint: disable=no-self-use
return attr
@property
def unique_id(self):
""" Returns the id of this ISY sensor. """
# pylint: disable=protected-access
return self.node._id
@property
def raw_name(self):
""" Returns the unclean node name. """
return str(self._name) \
if self._name is not None else str(self.node.name)
@property
def name(self):
""" Returns the cleaned name of the node. """
return self.raw_name.replace(HIDDEN_STRING, '').strip() \
.replace('_', ' ')
def update(self):
""" Update state of the sensor. """
# ISY objects are automatically updated by the ISY's event stream
pass
def on_update(self, event):
""" Handles the update received event. """
self.update_ha_state()
@property
def is_on(self):
""" Returns boolean response if the node is on. """
return bool(self.value)
@property
def is_open(self):
""" Returns boolean respons if the node is open. On = Open. """
return self.is_on
@property
def state(self):
""" Returns the state of the node. """
if len(self._states) > 0:
return self._states[0] if self.is_on else self._states[1]
return self.value
def turn_on(self, **kwargs):
""" Turns the device on. """
if self.domain is not 'sensor':
attrs = [kwargs.get(name) for name in self._onattrs]
self.node.on(*attrs)
else:
_LOGGER.error('ISY cannot turn on sensors.')
def turn_off(self, **kwargs):
""" Turns the device off. """
if self.domain is not 'sensor':
self.node.off()
else:
_LOGGER.error('ISY cannot turn off sensors.')
@property
def unit_of_measurement(self):
""" Returns the defined units of measurement or None. """
try:
return self.node.units
except AttributeError:
return None

View File

@@ -6,76 +6,82 @@ Provides functionality to emulate keyboard presses on host machine.
"""
import logging
import homeassistant.components as components
from homeassistant.const import (
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_MEDIA_PLAY_PAUSE)
DOMAIN = "keyboard"
DEPENDENCIES = []
REQUIREMENTS = ['pyuserinput==0.1.9']
def volume_up(bus):
def volume_up(hass):
""" Press the keyboard button for volume up. """
bus.call_service(DOMAIN, components.SERVICE_VOLUME_UP)
hass.services.call(DOMAIN, SERVICE_VOLUME_UP)
def volume_down(bus):
def volume_down(hass):
""" Press the keyboard button for volume down. """
bus.call_service(DOMAIN, components.SERVICE_VOLUME_DOWN)
hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN)
def volume_mute(bus):
def volume_mute(hass):
""" Press the keyboard button for muting volume. """
bus.call_service(DOMAIN, components.SERVICE_VOLUME_MUTE)
hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE)
def media_play_pause(bus):
def media_play_pause(hass):
""" Press the keyboard button for play/pause. """
bus.call_service(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE)
hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE)
def media_next_track(bus):
def media_next_track(hass):
""" Press the keyboard button for next track. """
bus.call_service(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK)
hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK)
def media_prev_track(bus):
def media_prev_track(hass):
""" Press the keyboard button for prev track. """
bus.call_service(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK)
hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK)
def setup(bus):
def setup(hass, config):
""" Listen for keyboard events. """
try:
import pykeyboard
except ImportError:
logging.getLogger(__name__).exception(
"MediaButtons: Error while importing dependency PyUserInput.")
"Error while importing dependency PyUserInput.")
return False
keyboard = pykeyboard.PyKeyboard()
keyboard.special_key_assignment()
bus.register_service(DOMAIN, components.SERVICE_VOLUME_UP,
lambda service:
keyboard.tap_key(keyboard.volume_up_key))
hass.services.register(DOMAIN, SERVICE_VOLUME_UP,
lambda service:
keyboard.tap_key(keyboard.volume_up_key))
bus.register_service(DOMAIN, components.SERVICE_VOLUME_DOWN,
lambda service:
keyboard.tap_key(keyboard.volume_down_key))
hass.services.register(DOMAIN, SERVICE_VOLUME_DOWN,
lambda service:
keyboard.tap_key(keyboard.volume_down_key))
bus.register_service(DOMAIN, components.SERVICE_VOLUME_MUTE,
lambda service:
keyboard.tap_key(keyboard.volume_mute_key))
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE,
lambda service:
keyboard.tap_key(keyboard.volume_mute_key))
bus.register_service(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE,
lambda service:
keyboard.tap_key(keyboard.media_play_pause_key))
hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE,
lambda service:
keyboard.tap_key(keyboard.media_play_pause_key))
bus.register_service(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK,
lambda service:
keyboard.tap_key(keyboard.media_next_track_key))
hass.services.register(DOMAIN, SERVICE_MEDIA_NEXT_TRACK,
lambda service:
keyboard.tap_key(keyboard.media_next_track_key))
bus.register_service(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK,
lambda service:
keyboard.tap_key(keyboard.media_prev_track_key))
hass.services.register(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK,
lambda service:
keyboard.tap_key(keyboard.media_prev_track_key))
return True

View File

@@ -1,6 +1,6 @@
"""
homeassistant.components.light
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to interact with lights.
@@ -49,30 +49,28 @@ Supports following parameters:
"""
import logging
import socket
from datetime import datetime, timedelta
from collections import namedtuple
import os
import csv
import homeassistant as ha
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import ToggleEntity
import homeassistant.util as util
from homeassistant.components import (group, extract_entity_ids,
STATE_ON, STATE_OFF,
SERVICE_TURN_ON, SERVICE_TURN_OFF,
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME)
import homeassistant.util.color as color_util
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.components import group, discovery, wink, isy994
DOMAIN = "light"
DEPENDENCIES = []
SCAN_INTERVAL = 30
GROUP_NAME_ALL_LIGHTS = 'all_lights'
ENTITY_ID_ALL_LIGHTS = group.ENTITY_ID_FORMAT.format(
GROUP_NAME_ALL_LIGHTS)
GROUP_NAME_ALL_LIGHTS = 'all lights'
ENTITY_ID_ALL_LIGHTS = group.ENTITY_ID_FORMAT.format('all_lights')
ENTITY_ID_FORMAT = DOMAIN + ".{}"
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
# integer that represents transition time in seconds to make change
ATTR_TRANSITION = "transition"
@@ -86,157 +84,107 @@ ATTR_BRIGHTNESS = "brightness"
# String representing a profile (built-in ones or external defined)
ATTR_PROFILE = "profile"
# If the light should flash, can be FLASH_SHORT or FLASH_LONG
ATTR_FLASH = "flash"
FLASH_SHORT = "short"
FLASH_LONG = "long"
# Apply an effect to the light, can be EFFECT_COLORLOOP
ATTR_EFFECT = "effect"
EFFECT_COLORLOOP = "colorloop"
LIGHT_PROFILES_FILE = "light_profiles.csv"
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
wink.DISCOVER_LIGHTS: 'wink',
isy994.DISCOVER_LIGHTS: 'isy994',
discovery.SERVICE_HUE: 'hue',
}
def is_on(statemachine, entity_id=None):
PROP_TO_ATTR = {
'brightness': ATTR_BRIGHTNESS,
'color_xy': ATTR_XY_COLOR,
}
_LOGGER = logging.getLogger(__name__)
def is_on(hass, entity_id=None):
""" Returns if the lights are on based on the statemachine. """
entity_id = entity_id or ENTITY_ID_ALL_LIGHTS
return statemachine.is_state(entity_id, STATE_ON)
return hass.states.is_state(entity_id, STATE_ON)
# pylint: disable=too-many-arguments
def turn_on(bus, entity_id=None, transition=None, brightness=None,
rgb_color=None, xy_color=None, profile=None):
def turn_on(hass, entity_id=None, transition=None, brightness=None,
rgb_color=None, xy_color=None, profile=None, flash=None,
effect=None):
""" Turns all or specified light on. """
data = {}
data = {
key: value for key, value in [
(ATTR_ENTITY_ID, entity_id),
(ATTR_PROFILE, profile),
(ATTR_TRANSITION, transition),
(ATTR_BRIGHTNESS, brightness),
(ATTR_RGB_COLOR, rgb_color),
(ATTR_XY_COLOR, xy_color),
(ATTR_FLASH, flash),
(ATTR_EFFECT, effect),
] if value is not None
}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
if profile:
data[ATTR_PROFILE] = profile
if transition is not None:
data[ATTR_TRANSITION] = transition
if brightness is not None:
data[ATTR_BRIGHTNESS] = brightness
if rgb_color:
data[ATTR_RGB_COLOR] = rgb_color
if xy_color:
data[ATTR_XY_COLOR] = xy_color
bus.call_service(DOMAIN, SERVICE_TURN_ON, data)
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
def turn_off(bus, entity_id=None, transition=None):
def turn_off(hass, entity_id=None, transition=None):
""" Turns all or specified light off. """
data = {}
data = {
key: value for key, value in [
(ATTR_ENTITY_ID, entity_id),
(ATTR_TRANSITION, transition),
] if value is not None
}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
if transition is not None:
data[ATTR_TRANSITION] = transition
bus.call_service(DOMAIN, SERVICE_TURN_OFF, data)
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
# pylint: disable=too-many-branches, too-many-locals
def setup(bus, statemachine, light_control):
def setup(hass, config):
""" Exposes light control via statemachine and services. """
logger = logging.getLogger(__name__)
ent_to_light = {}
light_to_ent = {}
def _update_light_state(light_id, light_state):
""" Update statemachine based on the LightState passed in. """
name = light_control.get_name(light_id) or "Unknown Light"
try:
entity_id = light_to_ent[light_id]
except KeyError:
# We have not seen this light before, set it up
# Create entity id
logger.info(u"Found new light {}".format(name))
entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(name)),
ent_to_light.keys())
ent_to_light[entity_id] = light_id
light_to_ent[light_id] = entity_id
state_attr = {ATTR_FRIENDLY_NAME: name}
if light_state.on:
state = STATE_ON
if light_state.brightness:
state_attr[ATTR_BRIGHTNESS] = light_state.brightness
if light_state.color:
state_attr[ATTR_XY_COLOR] = light_state.color
else:
state = STATE_OFF
statemachine.set_state(entity_id, state, state_attr)
def update_light_state(light_id):
""" Update the state of specified light. """
_update_light_state(light_id, light_control.get_state(light_id))
# pylint: disable=unused-argument
def update_lights_state(time, force_reload=False):
""" Update the state of all the lights. """
# First time this method gets called, force_reload should be True
if (force_reload or
datetime.now() - update_lights_state.last_updated >
MIN_TIME_BETWEEN_SCANS):
logger.info("Updating light status")
update_lights_state.last_updated = datetime.now()
for light_id, light_state in light_control.get_states().items():
_update_light_state(light_id, light_state)
# Update light state and discover lights for tracking the group
update_lights_state(None, True)
if len(ent_to_light) == 0:
logger.error("No lights found")
return False
# Track all lights in a group
group.setup(bus, statemachine,
GROUP_NAME_ALL_LIGHTS, light_to_ent.values())
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
GROUP_NAME_ALL_LIGHTS)
component.setup(config)
# Load built-in profiles and custom profiles
profile_paths = [os.path.dirname(__file__), os.getcwd()]
profile_paths = [os.path.join(os.path.dirname(__file__),
LIGHT_PROFILES_FILE),
hass.config.path(LIGHT_PROFILES_FILE)]
profiles = {}
for dir_path in profile_paths:
file_path = os.path.join(dir_path, LIGHT_PROFILES_FILE)
for profile_path in profile_paths:
if not os.path.isfile(profile_path):
continue
with open(profile_path) as inp:
reader = csv.reader(inp)
if os.path.isfile(file_path):
with open(file_path, 'rb') as inp:
reader = csv.reader(inp)
# Skip the header
next(reader, None)
# Skip the header
next(reader, None)
try:
for profile_id, color_x, color_y, brightness in reader:
profiles[profile_id] = (float(color_x), float(color_y),
int(brightness))
except ValueError:
# ValueError if not 4 values per row
# ValueError if convert to float/int failed
_LOGGER.error(
"Error parsing light profiles from %s", profile_path)
try:
for profile_id, color_x, color_y, brightness in reader:
profiles[profile_id] = (float(color_x), float(color_y),
int(brightness))
except ValueError:
# ValueError if not 4 values per row
# ValueError if convert to float/int failed
logger.error(
"Error parsing light profiles from {}".format(
file_path))
return False
return False
def handle_light_service(service):
""" Hande a turn light on or off service call. """
@@ -244,215 +192,131 @@ def setup(bus, statemachine, light_control):
dat = service.data
# Convert the entity ids to valid light ids
light_ids = [ent_to_light[entity_id] for entity_id
in extract_entity_ids(statemachine, service)
if entity_id in ent_to_light]
target_lights = component.extract_from_service(service)
if not light_ids:
light_ids = ent_to_light.values()
params = {}
transition = util.convert(dat.get(ATTR_TRANSITION), int)
if transition is not None:
params[ATTR_TRANSITION] = transition
if service.service == SERVICE_TURN_OFF:
light_control.turn_light_off(light_ids, transition)
for light in target_lights:
light.turn_off(**params)
else:
# Processing extra data for turn light on request
for light in target_lights:
if light.should_poll:
light.update_ha_state(True)
return
# We process the profile first so that we get the desired
# behavior that extra service data attributes overwrite
# profile values
profile = profiles.get(dat.get(ATTR_PROFILE))
# Processing extra data for turn light on request
if profile:
color = profile[0:2]
bright = profile[2]
else:
color = None
bright = None
# We process the profile first so that we get the desired
# behavior that extra service data attributes overwrite
# profile values
profile = profiles.get(dat.get(ATTR_PROFILE))
if ATTR_BRIGHTNESS in dat:
bright = util.convert(dat.get(ATTR_BRIGHTNESS), int)
if profile:
*params[ATTR_XY_COLOR], params[ATTR_BRIGHTNESS] = profile
if ATTR_XY_COLOR in dat:
try:
# xy_color should be a list containing 2 floats
xy_color = [float(val) for val in dat.get(ATTR_XY_COLOR)]
if ATTR_BRIGHTNESS in dat:
# We pass in the old value as the default parameter if parsing
# of the new one goes wrong.
params[ATTR_BRIGHTNESS] = util.convert(
dat.get(ATTR_BRIGHTNESS), int, params.get(ATTR_BRIGHTNESS))
if len(xy_color) == 2:
color = xy_color
if ATTR_XY_COLOR in dat:
try:
# xy_color should be a list containing 2 floats
xycolor = dat.get(ATTR_XY_COLOR)
except (TypeError, ValueError):
# TypeError if dat[ATTR_XY_COLOR] is not iterable
# ValueError if value could not be converted to float
pass
# Without this check, a xycolor with value '99' would work
if not isinstance(xycolor, str):
params[ATTR_XY_COLOR] = [float(val) for val in xycolor]
if ATTR_RGB_COLOR in dat:
try:
# rgb_color should be a list containing 3 ints
rgb_color = [int(val) for val in dat.get(ATTR_RGB_COLOR)]
except (TypeError, ValueError):
# TypeError if xy_color is not iterable
# ValueError if value could not be converted to float
pass
if len(rgb_color) == 3:
color = util.color_RGB_to_xy(rgb_color[0],
rgb_color[1],
rgb_color[2])
if ATTR_RGB_COLOR in dat:
try:
# rgb_color should be a list containing 3 ints
rgb_color = dat.get(ATTR_RGB_COLOR)
except (TypeError, ValueError):
# TypeError if dat[ATTR_RGB_COLOR] is not iterable
# ValueError if not all values can be converted to int
pass
if len(rgb_color) == 3:
params[ATTR_XY_COLOR] = \
color_util.color_RGB_to_xy(int(rgb_color[0]),
int(rgb_color[1]),
int(rgb_color[2]))
light_control.turn_light_on(light_ids, transition, bright, color)
except (TypeError, ValueError):
# TypeError if rgb_color is not iterable
# ValueError if not all values can be converted to int
pass
# Update state of lights touched. If there was only 1 light selected
# then just update that light else update all
if len(light_ids) == 1:
update_light_state(light_ids[0])
else:
update_lights_state(None, True)
if ATTR_FLASH in dat:
if dat[ATTR_FLASH] == FLASH_SHORT:
params[ATTR_FLASH] = FLASH_SHORT
# Update light state every 30 seconds
ha.track_time_change(bus, update_lights_state, second=[0, 30])
elif dat[ATTR_FLASH] == FLASH_LONG:
params[ATTR_FLASH] = FLASH_LONG
if ATTR_EFFECT in dat:
if dat[ATTR_EFFECT] == EFFECT_COLORLOOP:
params[ATTR_EFFECT] = EFFECT_COLORLOOP
for light in target_lights:
light.turn_on(**params)
for light in target_lights:
if light.should_poll:
light.update_ha_state(True)
# Listen for light on and light off service calls
bus.register_service(DOMAIN, SERVICE_TURN_ON,
handle_light_service)
hass.services.register(DOMAIN, SERVICE_TURN_ON,
handle_light_service)
bus.register_service(DOMAIN, SERVICE_TURN_OFF,
handle_light_service)
hass.services.register(DOMAIN, SERVICE_TURN_OFF,
handle_light_service)
return True
LightState = namedtuple("LightState", ['on', 'brightness', 'color'])
class Light(ToggleEntity):
""" Represents a light within Home Assistant. """
# pylint: disable=no-self-use
def _hue_to_light_state(info):
""" Helper method to convert a Hue state to a LightState. """
try:
return LightState(info['state']['reachable'] and info['state']['on'],
info['state']['bri'], info['state']['xy'])
except KeyError:
# KeyError if one of the keys didn't exist
@property
def brightness(self):
""" Brightness of this light between 0..255. """
return None
@property
def color_xy(self):
""" XY color value [float, float]. """
return None
class HueLightControl(object):
""" Class to interface with the Hue light system. """
@property
def device_state_attributes(self):
""" Returns device specific state attributes. """
return None
def __init__(self, host=None):
logger = logging.getLogger(__name__)
@property
def state_attributes(self):
""" Returns optional state attributes. """
data = {}
try:
import phue
except ImportError:
logger.exception(
"HueLightControl:Error while importing dependency phue.")
if self.is_on:
for prop, attr in PROP_TO_ATTR.items():
value = getattr(self, prop)
if value:
data[attr] = value
self.success_init = False
device_attr = self.device_state_attributes
return
if device_attr is not None:
data.update(device_attr)
try:
self._bridge = phue.Bridge(host)
except socket.error: # Error connecting using Phue
logger.exception((
"HueLightControl:Error while connecting to the bridge. "
"Is phue registered?"))
self.success_init = False
return
# Dict mapping light_id to name
self._lights = {}
self._update_lights()
if len(self._lights) == 0:
logger.error("HueLightControl:Could not find any lights. ")
self.success_init = False
else:
self.success_init = True
def _update_lights(self):
""" Helper method to update the known names from Hue. """
try:
self._lights = {int(item[0]): item[1]['name'] for item
in self._bridge.get_light().items()}
except (socket.error, KeyError):
# socket.error because sometimes we cannot reach Hue
# KeyError if we got unexpected data
# We don't do anything, keep old values
pass
def get_name(self, light_id):
""" Return name for specified light_id or None if no name known. """
if not light_id in self._lights:
self._update_lights()
return self._lights.get(light_id)
def get_state(self, light_id):
""" Return a LightState representing light light_id. """
try:
info = self._bridge.get_light(light_id)
return _hue_to_light_state(info)
except socket.error:
# socket.error when we cannot reach Hue
return None
def get_states(self):
""" Return a dict with id mapped to LightState objects. """
states = {}
try:
api = self._bridge.get_api()
except socket.error:
# socket.error when we cannot reach Hue
return states
api_states = api.get('lights')
if not isinstance(api_states, dict):
return states
for light_id, info in api_states.items():
state = _hue_to_light_state(info)
if state:
states[int(light_id)] = state
return states
def turn_light_on(self, light_ids, transition, brightness, xy_color):
""" Turn the specified or all lights on. """
command = {'on': True}
if transition is not None:
# Transition time is in 1/10th seconds and cannot exceed
# 900 seconds.
command['transitiontime'] = min(9000, transition * 10)
if brightness is not None:
command['bri'] = brightness
if xy_color:
command['xy'] = xy_color
self._bridge.set_light(light_ids, command)
def turn_light_off(self, light_ids, transition):
""" Turn the specified or all lights off. """
command = {'on': False}
if transition is not None:
# Transition time is in 1/10th seconds and cannot exceed
# 900 seconds.
command['transitiontime'] = min(9000, transition * 10)
self._bridge.set_light(light_ids, command)
return data

View File

@@ -0,0 +1,78 @@
"""
homeassistant.components.light.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Demo platform that implements lights.
"""
import random
from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR)
LIGHT_COLORS = [
[0.861, 0.3259],
[0.6389, 0.3028],
[0.1684, 0.0416]
]
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return demo lights. """
add_devices_callback([
DemoLight("Bed Light", False),
DemoLight("Ceiling", True),
DemoLight("Kitchen", True)
])
class DemoLight(Light):
""" Provides a demo switch. """
def __init__(self, name, state, xy=None, brightness=180):
self._name = name
self._state = state
self._xy = xy or random.choice(LIGHT_COLORS)
self._brightness = brightness
@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):
""" Brightness of this light between 0..255. """
return self._brightness
@property
def color_xy(self):
""" XY color value. """
return self._xy
@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
if ATTR_XY_COLOR in kwargs:
self._xy = kwargs[ATTR_XY_COLOR]
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
self.update_ha_state()
def turn_off(self, **kwargs):
""" Turn the device off. """
self._state = False
self.update_ha_state()

View File

@@ -0,0 +1,222 @@
"""
homeassistant.components.light.hue
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for Hue lights.
"""
import logging
import socket
from datetime import timedelta
from urllib.parse import urlparse
from homeassistant.loader import get_component
import homeassistant.util as util
from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME
from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION,
ATTR_FLASH, FLASH_LONG, FLASH_SHORT, ATTR_EFFECT,
EFFECT_COLORLOOP)
REQUIREMENTS = ['phue==0.8']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
PHUE_CONFIG_FILE = "phue.conf"
# Map ip to request id for configuring
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Gets the Hue lights. """
try:
# pylint: disable=unused-variable
import phue # noqa
except ImportError:
_LOGGER.exception("Error while importing dependency phue.")
return
if discovery_info is not None:
host = urlparse(discovery_info[1]).hostname
else:
host = config.get(CONF_HOST, None)
# Only act if we are not already configuring this host
if host in _CONFIGURING:
return
setup_bridge(host, hass, add_devices_callback)
def setup_bridge(host, hass, add_devices_callback):
""" Setup a phue bridge based on host parameter. """
import phue
try:
bridge = phue.Bridge(
host,
config_file_path=hass.config.path(PHUE_CONFIG_FILE))
except ConnectionRefusedError: # Wrong host was given
_LOGGER.exception("Error connecting to the Hue bridge at %s", host)
return
except phue.PhueRegistrationException:
_LOGGER.warning("Connected to Hue at %s but not registered.", host)
request_configuration(host, hass, add_devices_callback)
return
# If we came here and configuring this host, mark as done
if host in _CONFIGURING:
request_id = _CONFIGURING.pop(host)
configurator = get_component('configurator')
configurator.request_done(request_id)
lights = {}
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_lights():
""" Updates the Hue light objects with latest info from the bridge. """
try:
api = bridge.get_api()
except socket.error:
# socket.error when we cannot reach Hue
_LOGGER.exception("Cannot reach the bridge")
return
api_states = api.get('lights')
if not isinstance(api_states, dict):
_LOGGER.error("Got unexpected result from Hue API")
return
new_lights = []
for light_id, info in api_states.items():
if light_id not in lights:
lights[light_id] = HueLight(int(light_id), info,
bridge, update_lights)
new_lights.append(lights[light_id])
else:
lights[light_id].info = info
if new_lights:
add_devices_callback(new_lights)
update_lights()
def request_configuration(host, hass, add_devices_callback):
""" Request configuration steps from the user. """
configurator = get_component('configurator')
# We got an error if this method is called while we are configuring
if host in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING[host], "Failed to register, please try again.")
return
def hue_configuration_callback(data):
""" Actions to do when our configuration callback is called. """
setup_bridge(host, hass, add_devices_callback)
_CONFIGURING[host] = configurator.request_config(
hass, "Philips Hue", hue_configuration_callback,
description=("Press the button on the bridge to register Philips Hue "
"with Home Assistant."),
description_image="/static/images/config_philips_hue.jpg",
submit_caption="I have pressed the button"
)
class HueLight(Light):
""" Represents a Hue light """
def __init__(self, light_id, info, bridge, update_lights):
self.light_id = light_id
self.info = info
self.bridge = bridge
self.update_lights = update_lights
@property
def unique_id(self):
""" Returns the id of this Hue light """
return "{}.{}".format(
self.__class__, self.info.get('uniqueid', self.name))
@property
def name(self):
""" Get the mame of the Hue light. """
return self.info.get('name', DEVICE_DEFAULT_NAME)
@property
def brightness(self):
""" Brightness of this light between 0..255. """
return self.info['state']['bri']
@property
def color_xy(self):
""" XY color value. """
return self.info['state'].get('xy')
@property
def is_on(self):
""" True if device is on. """
self.update_lights()
return self.info['state']['reachable'] and self.info['state']['on']
def turn_on(self, **kwargs):
""" Turn the specified or all lights on. """
command = {'on': True}
if ATTR_TRANSITION in kwargs:
# Transition time is in 1/10th seconds and cannot exceed
# 900 seconds.
command['transitiontime'] = min(9000, kwargs[ATTR_TRANSITION] * 10)
if ATTR_BRIGHTNESS in kwargs:
command['bri'] = kwargs[ATTR_BRIGHTNESS]
if ATTR_XY_COLOR in kwargs:
command['xy'] = kwargs[ATTR_XY_COLOR]
flash = kwargs.get(ATTR_FLASH)
if flash == FLASH_LONG:
command['alert'] = 'lselect'
elif flash == FLASH_SHORT:
command['alert'] = 'select'
else:
command['alert'] = 'none'
effect = kwargs.get(ATTR_EFFECT)
if effect == EFFECT_COLORLOOP:
command['effect'] = 'colorloop'
else:
command['effect'] = 'none'
self.bridge.set_light(self.light_id, command)
def turn_off(self, **kwargs):
""" Turn the specified or all lights off. """
command = {'on': False}
if ATTR_TRANSITION in kwargs:
# Transition time is in 1/10th seconds and cannot exceed
# 900 seconds.
command['transitiontime'] = min(9000, kwargs[ATTR_TRANSITION] * 10)
self.bridge.set_light(self.light_id, command)
def update(self):
""" Synchronize state with bridge. """
self.update_lights(no_throttle=True)

View File

@@ -0,0 +1,46 @@
"""
homeassistant.components.light.isy994
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for ISY994 lights.
"""
import logging
from homeassistant.components.isy994 import (ISYDeviceABC, ISY, SENSOR_STRING,
HIDDEN_STRING)
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.const import STATE_ON, STATE_OFF
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the ISY994 platform. """
logger = logging.getLogger(__name__)
devs = []
# verify connection
if ISY is None or not ISY.connected:
logger.error('A connection has not been made to the ISY controller.')
return False
# import dimmable nodes
for (path, node) in ISY.nodes:
if node.dimmable and SENSOR_STRING not in node.name:
if HIDDEN_STRING in path:
node.name += HIDDEN_STRING
devs.append(ISYLightDevice(node))
add_devices(devs)
class ISYLightDevice(ISYDeviceABC):
""" Represents as ISY light. """
_domain = 'light'
_dtype = 'analog'
_attrs = {ATTR_BRIGHTNESS: 'value'}
_onattrs = [ATTR_BRIGHTNESS]
_states = [STATE_ON, STATE_OFF]
def _attr_filter(self, attr):
""" Filter brightness out of entity while off. """
if ATTR_BRIGHTNESS in attr and not self.is_on:
del attr[ATTR_BRIGHTNESS]
return attr

View File

@@ -0,0 +1,144 @@
"""
homeassistant.components.light.limitlessled
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for LimitlessLED bulbs, also known as...
- EasyBulb
- AppLight
- AppLamp
- MiLight
- LEDme
- dekolight
- iLight
Configuration:
To use limitlessled you will need to add the following to your
config/configuration.yaml.
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
"""
import logging
from homeassistant.const import DEVICE_DEFAULT_NAME
from homeassistant.components.light import (Light, ATTR_BRIGHTNESS,
ATTR_XY_COLOR)
from homeassistant.util.color import color_RGB_to_xy
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['ledcontroller==1.0.7']
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Gets the LimitlessLED lights. """
import ledcontroller
led = ledcontroller.LedController(config['host'])
lights = []
for i in range(1, 5):
if 'group_%d_name' % (i) in config:
lights.append(LimitlessLED(led, i, config['group_%d_name' % (i)]))
add_devices_callback(lights)
class LimitlessLED(Light):
""" Represents a LimitlessLED light """
def __init__(self, led, group, name):
self.led = led
self.group = group
# LimitlessLEDs don't report state, we have track it ourselves.
self.led.off(self.group)
self._name = name or DEVICE_DEFAULT_NAME
self._state = False
self._brightness = 100
self._xy_color = color_RGB_to_xy(255, 255, 255)
# Build a color table that maps an RGB color to a color string
# recognized by LedController's set_color method
self._color_table = [(color_RGB_to_xy(*x[0]), x[1]) for x in [
((0xFF, 0xFF, 0xFF), 'white'),
((0xEE, 0x82, 0xEE), 'violet'),
((0x41, 0x69, 0xE1), 'royal_blue'),
((0x87, 0xCE, 0xFA), 'baby_blue'),
((0x00, 0xFF, 0xFF), 'aqua'),
((0x7F, 0xFF, 0xD4), 'royal_mint'),
((0x2E, 0x8B, 0x57), 'seafoam_green'),
((0x00, 0x80, 0x00), 'green'),
((0x32, 0xCD, 0x32), 'lime_green'),
((0xFF, 0xFF, 0x00), 'yellow'),
((0xDA, 0xA5, 0x20), 'yellow_orange'),
((0xFF, 0xA5, 0x00), 'orange'),
((0xFF, 0x00, 0x00), 'red'),
((0xFF, 0xC0, 0xCB), 'pink'),
((0xFF, 0x00, 0xFF), 'fusia'),
((0xDA, 0x70, 0xD6), 'lilac'),
((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
@property
def color_xy(self):
return self._xy_color
def _xy_to_led_color(self, xy_color):
""" Convert an XY color to the closest LedController color string """
def abs_dist_squared(p_0, p_1):
""" Returns the absolute value of the squared distance """
return abs((p_0[0] - p_1[0])**2 + (p_0[1] - p_1[1])**2)
candidates = [(abs_dist_squared(xy_color, x[0]), x[1]) for x in
self._color_table]
# 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
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
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.update_ha_state()
def turn_off(self, **kwargs):
""" Turn the device off. """
self._state = False
self.led.off(self.group)
self.update_ha_state()

View File

@@ -0,0 +1,93 @@
"""
homeassistant.components.light.tellstick
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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
import tellcore.constants as tellcore_constants
REQUIREMENTS = ['tellcore-py==1.0.4']
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return Tellstick lights. """
try:
import tellcore.telldus as telldus
except ImportError:
logging.getLogger(__name__).exception(
"Failed to import tellcore")
return []
core = telldus.TelldusCore()
switches_and_lights = core.devices()
lights = []
for switch in switches_and_lights:
if switch.methods(tellcore_constants.TELLSTICK_DIM):
lights.append(TellstickLight(switch))
add_devices_callback(lights)
class TellstickLight(Light):
""" Represents a Tellstick light. """
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
tellcore_constants.TELLSTICK_TURNOFF |
tellcore_constants.TELLSTICK_DIM |
tellcore_constants.TELLSTICK_UP |
tellcore_constants.TELLSTICK_DOWN)
def __init__(self, tellstick):
self.tellstick = tellstick
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
self._brightness = 0
@property
def name(self):
""" Returns the name of the switch if any. """
return self.tellstick.name
@property
def is_on(self):
""" True if switch is on. """
return self._brightness > 0
@property
def brightness(self):
""" Brightness of this light between 0..255. """
return self._brightness
def turn_off(self, **kwargs):
""" Turns the switch off. """
self.tellstick.turn_off()
self._brightness = 0
def turn_on(self, **kwargs):
""" Turns the switch on. """
brightness = kwargs.get(ATTR_BRIGHTNESS)
if brightness is None:
self._brightness = 255
else:
self._brightness = brightness
self.tellstick.dim(self._brightness)
def update(self):
""" Update state of the light. """
last_command = self.tellstick.last_sent_command(
self.last_sent_command_mask)
if last_command == tellcore_constants.TELLSTICK_TURNON:
self._brightness = 255
elif last_command == tellcore_constants.TELLSTICK_TURNOFF:
self._brightness = 0
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()
if last_sent_value is not None:
self._brightness = last_sent_value

View File

@@ -0,0 +1,92 @@
"""
homeassistant.components.light.vera
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for Vera lights. This component is useful if you wish for switches
connected to your Vera controller to appear as lights in Home Assistant.
All switches will be added as a light unless you exclude them in the config.
Configuration:
To use the Vera lights you will need to add something like the following to
your config/configuration.yaml.
light:
platform: vera
vera_controller_url: http://YOUR_VERA_IP:3480/
device_data:
12:
name: My awesome switch
exclude: true
13:
name: Another switch
Variables:
vera_controller_url
*Required
This is the base URL of your vera controller including the port number if not
running on 80. Example: http://192.168.1.21:3480/
device_data
*Optional
This contains an array additional device info for your Vera devices. It is not
required and if not specified all lights configured in your Vera controller
will be added with default values. You should use the id of your vera device
as the key for the device within device_data.
These are the variables for the device_data array:
name
*Optional
This parameter allows you to override the name of your Vera device in the HA
interface, if not specified the value configured for the device in your Vera
will be used.
exclude
*Optional
This parameter allows you to exclude the specified device from Home Assistant,
it should be set to "true" if you want this device excluded.
"""
import logging
from requests.exceptions import RequestException
from homeassistant.components.switch.vera import VeraSwitch
# pylint: disable=no-name-in-module, import-error
import homeassistant.external.vera.vera as veraApi
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return Vera lights. """
base_url = config.get('vera_controller_url')
if not base_url:
_LOGGER.error(
"The required parameter 'vera_controller_url'"
" was not found in config"
)
return False
device_data = config.get('device_data', {})
controller = veraApi.VeraController(base_url)
devices = []
try:
devices = controller.get_devices(['Switch', 'On/Off Switch'])
except RequestException:
# There was a network related error connecting to the vera controller
_LOGGER.exception("Error communicating with Vera API")
return False
lights = []
for device in devices:
extra_data = device_data.get(device.deviceId, {})
exclude = extra_data.get('exclude', False)
if exclude is not True:
lights.append(VeraSwitch(device, extra_data))
add_devices_callback(lights)

View File

@@ -0,0 +1,59 @@
"""
homeassistant.components.light.wink
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for Wink lights.
"""
import logging
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.components.wink import WinkToggleDevice
from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' +
'c2b700e8ca866159566ecf5e644d9c297f69f257.zip']
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return Wink lights. """
import pywink
token = config.get(CONF_ACCESS_TOKEN)
if not pywink.is_token_set() and token is None:
logging.getLogger(__name__).error(
"Missing wink access_token - "
"get one at https://winkbearertoken.appspot.com/")
return
elif token is not None:
pywink.set_bearer_token(token)
add_devices_callback(
WinkLight(light) for light in pywink.get_bulbs())
class WinkLight(WinkToggleDevice):
""" Represents a Wink light. """
# pylint: disable=too-few-public-methods
def turn_on(self, **kwargs):
""" Turns the switch on. """
brightness = kwargs.get(ATTR_BRIGHTNESS)
if brightness is not None:
self.wink.setState(True, brightness / 255)
else:
self.wink.setState(True)
@property
def state_attributes(self):
attr = super().state_attributes
if self.is_on:
brightness = self.wink.brightness()
if brightness is not None:
attr[ATTR_BRIGHTNESS] = int(brightness * 255)
return attr

View File

@@ -0,0 +1,200 @@
"""
homeassistant.components.logbook
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Parses events and generates a human log.
"""
from datetime import timedelta
from itertools import groupby
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_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST)
import homeassistant.util.dt as dt_util
import homeassistant.components.recorder as recorder
import homeassistant.components.sun as sun
DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http']
URL_LOGBOOK = re.compile(r'/api/logbook(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
QUERY_EVENTS_BETWEEN = """
SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
"""
GROUP_BY_MINUTES = 15
def setup(hass, config):
""" Listens for download events to download files. """
hass.http.register_path('GET', URL_LOGBOOK, _handle_get_logbook)
return True
def _handle_get_logbook(handler, path_match, data):
""" Return logbook entries. """
date_str = path_match.group('date')
if date_str:
start_date = dt_util.date_str_to_date(date_str)
if start_date is None:
handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST)
return
start_day = dt_util.start_of_local_day(start_date)
else:
start_day = dt_util.start_of_local_day()
end_day = start_day + timedelta(days=1)
events = recorder.query_events(
QUERY_EVENTS_BETWEEN,
(dt_util.as_utc(start_day), dt_util.as_utc(end_day)))
handler.write_json(humanify(events))
class Entry(object):
""" A human readable version of the log. """
# pylint: disable=too-many-arguments, too-few-public-methods
def __init__(self, when=None, name=None, message=None, domain=None,
entity_id=None):
self.when = when
self.name = name
self.message = message
self.domain = domain
self.entity_id = entity_id
def as_dict(self):
""" Convert Entry to a dict to be used within JSON. """
return {
'when': dt_util.datetime_to_str(self.when),
'name': self.name,
'message': self.message,
'domain': self.domain,
'entity_id': self.entity_id,
}
def humanify(events):
"""
Generator that converts a list of events into Entry objects.
Will try to group events if possible:
- if 2+ sensor updates in GROUP_BY_MINUTES, show last
- if home assistant stop and start happen in same minute call it restarted
"""
# pylint: disable=too-many-branches
# Group events in batches of GROUP_BY_MINUTES
for _, g_events in groupby(
events,
lambda event: event.time_fired.minute // GROUP_BY_MINUTES):
events_batch = list(g_events)
# Keep track of last sensor states
last_sensor_event = {}
# group HA start/stop events
# Maps minute of event to 1: stop, 2: stop + start
start_stop_events = {}
# Process events
for event in events_batch:
if event.event_type == EVENT_STATE_CHANGED:
entity_id = event.data['entity_id']
if entity_id.startswith('sensor.'):
last_sensor_event[entity_id] = event
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
if event.time_fired.minute in start_stop_events:
continue
start_stop_events[event.time_fired.minute] = 1
elif event.event_type == EVENT_HOMEASSISTANT_START:
if event.time_fired.minute not in start_stop_events:
continue
start_stop_events[event.time_fired.minute] = 2
# Yield entries
for event in events_batch:
if event.event_type == EVENT_STATE_CHANGED:
# Do not report on new entities
if 'old_state' not in event.data:
continue
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 not to_state or \
to_state.last_changed != to_state.last_updated:
continue
domain = to_state.domain
# Skip all but the last sensor state
if domain == 'sensor' and \
event != last_sensor_event[to_state.entity_id]:
continue
yield Entry(
event.time_fired,
name=to_state.name,
message=_entry_message_from_state(domain, to_state),
domain=domain,
entity_id=to_state.entity_id)
elif event.event_type == EVENT_HOMEASSISTANT_START:
if start_stop_events.get(event.time_fired.minute) == 2:
continue
yield Entry(
event.time_fired, "Home Assistant", "started",
domain=HA_DOMAIN)
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
if start_stop_events.get(event.time_fired.minute) == 2:
action = "restarted"
else:
action = "stopped"
yield Entry(
event.time_fired, "Home Assistant", action,
domain=HA_DOMAIN)
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
if domain == 'device_tracker':
return '{} home'.format(
'arrived' if state.state == STATE_HOME else 'left')
elif domain == 'sun':
if state.state == sun.STATE_ABOVE_HORIZON:
return 'has risen'
else:
return 'has set'
elif state.state == STATE_ON:
# Future: combine groups and its entity entries ?
return "turned on"
elif state.state == STATE_OFF:
return "turned off"
return "changed to {}".format(state.state)

View File

@@ -0,0 +1,497 @@
"""
homeassistant.components.media_player
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Component to interface with various media players.
"""
import logging
from homeassistant.components import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.const import (
STATE_OFF, STATE_UNKNOWN, STATE_PLAYING,
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET,
SERVICE_VOLUME_MUTE,
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
DOMAIN = 'media_player'
DEPENDENCIES = []
SCAN_INTERVAL = 30
ENTITY_ID_FORMAT = DOMAIN + '.{}'
DISCOVERY_PLATFORMS = {
discovery.SERVICE_CAST: 'cast',
}
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video'
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted'
ATTR_MEDIA_SEEK_POSITION = 'seek_position'
ATTR_MEDIA_CONTENT_ID = 'media_content_id'
ATTR_MEDIA_CONTENT_TYPE = 'media_content_type'
ATTR_MEDIA_DURATION = 'media_duration'
ATTR_MEDIA_TITLE = 'media_title'
ATTR_MEDIA_ARTIST = 'media_artist'
ATTR_MEDIA_ALBUM_NAME = 'media_album_name'
ATTR_MEDIA_ALBUM_ARTIST = 'media_album_artist'
ATTR_MEDIA_TRACK = 'media_track'
ATTR_MEDIA_SERIES_TITLE = 'media_series_title'
ATTR_MEDIA_SEASON = 'media_season'
ATTR_MEDIA_EPISODE = 'media_episode'
ATTR_APP_ID = 'app_id'
ATTR_APP_NAME = 'app_name'
ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands'
MEDIA_TYPE_MUSIC = 'music'
MEDIA_TYPE_TVSHOW = 'tvshow'
MEDIA_TYPE_VIDEO = 'movie'
SUPPORT_PAUSE = 1
SUPPORT_SEEK = 2
SUPPORT_VOLUME_SET = 4
SUPPORT_VOLUME_MUTE = 8
SUPPORT_PREVIOUS_TRACK = 16
SUPPORT_NEXT_TRACK = 32
SUPPORT_YOUTUBE = 64
SUPPORT_TURN_ON = 128
SUPPORT_TURN_OFF = 256
YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg'
SERVICE_TO_METHOD = {
SERVICE_TURN_ON: 'turn_on',
SERVICE_TURN_OFF: 'turn_off',
SERVICE_VOLUME_UP: 'volume_up',
SERVICE_VOLUME_DOWN: 'volume_down',
SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause',
SERVICE_MEDIA_PLAY: 'media_play',
SERVICE_MEDIA_PAUSE: 'media_pause',
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
}
ATTR_TO_PROPERTY = [
ATTR_MEDIA_VOLUME_LEVEL,
ATTR_MEDIA_VOLUME_MUTED,
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
ATTR_MEDIA_DURATION,
ATTR_MEDIA_TITLE,
ATTR_MEDIA_ARTIST,
ATTR_MEDIA_ALBUM_NAME,
ATTR_MEDIA_ALBUM_ARTIST,
ATTR_MEDIA_TRACK,
ATTR_MEDIA_SERIES_TITLE,
ATTR_MEDIA_SEASON,
ATTR_MEDIA_EPISODE,
ATTR_APP_ID,
ATTR_APP_NAME,
ATTR_SUPPORTED_MEDIA_COMMANDS,
]
def is_on(hass, entity_id=None):
""" Returns true if specified media player entity_id is on.
Will check all media player if no entity_id specified. """
entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN)
return any(not hass.states.is_state(entity_id, STATE_OFF)
for entity_id in entity_ids)
def turn_on(hass, entity_id=None):
""" Will turn on specified media player or all. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
def turn_off(hass, entity_id=None):
""" Will turn off specified media player or all. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
def volume_up(hass, entity_id=None):
""" Send the media player the command for volume up. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_VOLUME_UP, data)
def volume_down(hass, entity_id=None):
""" Send the media player the command for volume down. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data)
def mute_volume(hass, mute, entity_id=None):
""" Send the media player the command for volume down. """
data = {ATTR_MEDIA_VOLUME_MUTED: mute}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, data)
def set_volume_level(hass, volume, entity_id=None):
""" Send the media player the command for volume down. """
data = {ATTR_MEDIA_VOLUME_LEVEL: volume}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_VOLUME_SET, data)
def media_play_pause(hass, entity_id=None):
""" Send the media player the command for play/pause. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, data)
def media_play(hass, entity_id=None):
""" Send the media player the command for play/pause. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY, data)
def media_pause(hass, entity_id=None):
""" Send the media player the command for play/pause. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data)
def media_next_track(hass, entity_id=None):
""" Send the media player the command for next track. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data)
def media_previous_track(hass, entity_id=None):
""" Send the media player the command for prev track. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data)
def setup(hass, config):
""" Track states and offer events for media_players. """
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS)
component.setup(config)
def media_player_service_handler(service):
""" Maps services to methods on MediaPlayerDevice. """
target_players = component.extract_from_service(service)
method = SERVICE_TO_METHOD[service.service]
for player in target_players:
getattr(player, method)()
if player.should_poll:
player.update_ha_state(True)
for service in SERVICE_TO_METHOD:
hass.services.register(DOMAIN, service, media_player_service_handler)
def volume_set_service(service):
""" Set specified volume on the media player. """
target_players = component.extract_from_service(service)
if ATTR_MEDIA_VOLUME_LEVEL not in service.data:
return
volume = service.data[ATTR_MEDIA_VOLUME_LEVEL]
for player in target_players:
player.set_volume_level(volume)
if player.should_poll:
player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service)
def volume_mute_service(service):
""" Mute (true) or unmute (false) the media player. """
target_players = component.extract_from_service(service)
if ATTR_MEDIA_VOLUME_MUTED not in service.data:
return
mute = service.data[ATTR_MEDIA_VOLUME_MUTED]
for player in target_players:
player.mute_volume(mute)
if player.should_poll:
player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, volume_mute_service)
def media_seek_service(service):
""" Seek to a position. """
target_players = component.extract_from_service(service)
if ATTR_MEDIA_SEEK_POSITION not in service.data:
return
position = service.data[ATTR_MEDIA_SEEK_POSITION]
for player in target_players:
player.seek(position)
if player.should_poll:
player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service)
def play_youtube_video_service(service, media_id=None):
""" Plays specified media_id on the media player. """
if media_id is None:
service.data.get('video')
if media_id is None:
return
for player in component.extract_from_service(service):
player.play_youtube(media_id)
if player.should_poll:
player.update_ha_state(True)
hass.services.register(
DOMAIN, "start_fireplace",
lambda service: play_youtube_video_service(service, "eyU3bRy2x44"))
hass.services.register(
DOMAIN, "start_epic_sax",
lambda service: play_youtube_video_service(service, "kxopViU98Xo"))
hass.services.register(
DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service)
return True
class MediaPlayerDevice(Entity):
""" ABC for media player devices. """
# pylint: disable=too-many-public-methods,no-self-use
# Implement these for your media player
@property
def state(self):
""" State of the player. """
return STATE_UNKNOWN
@property
def volume_level(self):
""" Volume level of the media player (0..1). """
return None
@property
def is_volume_muted(self):
""" Boolean if volume is currently muted. """
return None
@property
def media_content_id(self):
""" Content ID of current playing media. """
return None
@property
def media_content_type(self):
""" Content type of current playing media. """
return None
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return None
@property
def media_image_url(self):
""" Image url of current playing media. """
return None
@property
def media_title(self):
""" Title of current playing media. """
return None
@property
def media_artist(self):
""" Artist of current playing media. (Music track only) """
return None
@property
def media_album_name(self):
""" Album name of current playing media. (Music track only) """
return None
@property
def media_album_artist(self):
""" Album arist of current playing media. (Music track only) """
return None
@property
def media_track(self):
""" Track number of current playing media. (Music track only) """
return None
@property
def media_series_title(self):
""" Series title of current playing media. (TV Show only)"""
return None
@property
def media_season(self):
""" Season of current playing media. (TV Show only) """
return None
@property
def media_episode(self):
""" Episode of current playing media. (TV Show only) """
return None
@property
def app_id(self):
""" ID of the current running app. """
return None
@property
def app_name(self):
""" Name of the current running app. """
return None
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return 0
@property
def device_state_attributes(self):
""" Extra attributes a device wants to expose. """
return None
def turn_on(self):
""" turn the media player on. """
raise NotImplementedError()
def turn_off(self):
""" turn the media player off. """
raise NotImplementedError()
def mute_volume(self, mute):
""" mute the volume. """
raise NotImplementedError()
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
raise NotImplementedError()
def media_play(self):
""" Send play commmand. """
raise NotImplementedError()
def media_pause(self):
""" Send pause command. """
raise NotImplementedError()
def media_previous_track(self):
""" Send previous track command. """
raise NotImplementedError()
def media_next_track(self):
""" Send next track command. """
raise NotImplementedError()
def media_seek(self, position):
""" Send seek command. """
raise NotImplementedError()
def play_youtube(self, media_id):
""" Plays a YouTube media. """
raise NotImplementedError()
# No need to overwrite these.
@property
def support_pause(self):
""" Boolean if pause is supported. """
return bool(self.supported_media_commands & SUPPORT_PAUSE)
@property
def support_seek(self):
""" Boolean if seek is supported. """
return bool(self.supported_media_commands & SUPPORT_SEEK)
@property
def support_volume_set(self):
""" Boolean if setting volume is supported. """
return bool(self.supported_media_commands & SUPPORT_VOLUME_SET)
@property
def support_volume_mute(self):
""" Boolean if muting volume is supported. """
return bool(self.supported_media_commands & SUPPORT_VOLUME_MUTE)
@property
def support_previous_track(self):
""" Boolean if previous track command supported. """
return bool(self.supported_media_commands & SUPPORT_PREVIOUS_TRACK)
@property
def support_next_track(self):
""" Boolean if next track command supported. """
return bool(self.supported_media_commands & SUPPORT_NEXT_TRACK)
@property
def support_youtube(self):
""" Boolean if YouTube is supported. """
return bool(self.supported_media_commands & SUPPORT_YOUTUBE)
def volume_up(self):
""" volume_up media player. """
if self.volume_level < 1:
self.set_volume_level(min(1, self.volume_level + .1))
def volume_down(self):
""" volume_down media player. """
if self.volume_level > 0:
self.set_volume_level(max(0, self.volume_level - .1))
def media_play_pause(self):
""" media_play_pause media player. """
if self.state == STATE_PLAYING:
self.media_pause()
else:
self.media_play()
@property
def state_attributes(self):
""" Return the state attributes. """
if self.state == STATE_OFF:
state_attr = {
ATTR_SUPPORTED_MEDIA_COMMANDS: self.supported_media_commands,
}
else:
state_attr = {
attr: getattr(self, attr) for attr
in ATTR_TO_PROPERTY if getattr(self, attr)
}
if self.media_image_url:
state_attr[ATTR_ENTITY_PICTURE] = self.media_image_url
device_attr = self.device_state_attributes
if device_attr:
state_attr.update(device_attr)
return state_attr

View File

@@ -0,0 +1,277 @@
"""
homeassistant.components.media_player.chromecast
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to interact with Cast devices on the network.
WARNING: This platform is currently not working due to a changed Cast API
"""
import logging
from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_OFF,
STATE_UNKNOWN, CONF_HOST)
from homeassistant.components.media_player import (
MediaPlayerDevice,
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_YOUTUBE,
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
REQUIREMENTS = ['pychromecast==0.6.10']
CONF_IGNORE_CEC = 'ignore_cec'
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_YOUTUBE
KNOWN_HOSTS = []
# pylint: disable=invalid-name
cast = None
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the cast platform. """
global cast
import pychromecast
cast = pychromecast
logger = logging.getLogger(__name__)
# import CEC IGNORE attributes
ignore_cec = config.get(CONF_IGNORE_CEC, [])
if isinstance(ignore_cec, list):
cast.IGNORE_CEC += ignore_cec
else:
logger.error('Chromecast conig, %s must be a list.', CONF_IGNORE_CEC)
hosts = []
if discovery_info and discovery_info[0] not in KNOWN_HOSTS:
hosts = [discovery_info[0]]
elif CONF_HOST in config:
hosts = [config[CONF_HOST]]
else:
hosts = (host_port[0] for host_port
in cast.discover_chromecasts()
if host_port[0] not in KNOWN_HOSTS)
casts = []
for host in hosts:
try:
casts.append(CastDevice(host))
except cast.ChromecastConnectionError:
pass
else:
KNOWN_HOSTS.append(host)
add_devices(casts)
class CastDevice(MediaPlayerDevice):
""" Represents a Cast device on the network. """
# pylint: disable=too-many-public-methods
def __init__(self, host):
import pychromecast.controllers.youtube as youtube
self.cast = cast.Chromecast(host)
self.youtube = youtube.YouTubeController()
self.cast.register_handler(self.youtube)
self.cast.socket_client.receiver_controller.register_status_listener(
self)
self.cast.socket_client.media_controller.register_status_listener(self)
self.cast_status = self.cast.status
self.media_status = self.cast.media_controller.status
# Entity properties and methods
@property
def should_poll(self):
return False
@property
def name(self):
""" Returns the name of the device. """
return self.cast.device.friendly_name
# MediaPlayerDevice properties and methods
@property
def state(self):
""" State of the player. """
if self.media_status is None:
return STATE_UNKNOWN
elif self.media_status.player_is_playing:
return STATE_PLAYING
elif self.media_status.player_is_paused:
return STATE_PAUSED
elif self.media_status.player_is_idle:
return STATE_IDLE
elif self.cast.is_idle:
return STATE_OFF
else:
return STATE_UNKNOWN
@property
def volume_level(self):
""" Volume level of the media player (0..1). """
return self.cast_status.volume_level if self.cast_status else None
@property
def is_volume_muted(self):
""" Boolean if volume is currently muted. """
return self.cast_status.volume_muted if self.cast_status else None
@property
def media_content_id(self):
""" Content ID of current playing media. """
return self.media_status.content_id if self.media_status else None
@property
def media_content_type(self):
""" Content type of current playing media. """
if self.media_status is None:
return None
elif self.media_status.media_is_tvshow:
return MEDIA_TYPE_TVSHOW
elif self.media_status.media_is_movie:
return MEDIA_TYPE_VIDEO
elif self.media_status.media_is_musictrack:
return MEDIA_TYPE_MUSIC
return None
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return self.media_status.duration if self.media_status else None
@property
def media_image_url(self):
""" Image url of current playing media. """
if self.media_status is None:
return None
images = self.media_status.images
return images[0].url if images else None
@property
def media_title(self):
""" Title of current playing media. """
return self.media_status.title if self.media_status else None
@property
def media_artist(self):
""" Artist of current playing media. (Music track only) """
return self.media_status.artist if self.media_status else None
@property
def media_album(self):
""" Album of current playing media. (Music track only) """
return self.media_status.album_name if self.media_status else None
@property
def media_album_artist(self):
""" Album arist of current playing media. (Music track only) """
return self.media_status.album_artist if self.media_status else None
@property
def media_track(self):
""" Track number of current playing media. (Music track only) """
return self.media_status.track if self.media_status else None
@property
def media_series_title(self):
""" Series title of current playing media. (TV Show only)"""
return self.media_status.series_title if self.media_status else None
@property
def media_season(self):
""" Season of current playing media. (TV Show only) """
return self.media_status.season if self.media_status else None
@property
def media_episode(self):
""" Episode of current playing media. (TV Show only) """
return self.media_status.episode if self.media_status else None
@property
def app_id(self):
""" ID of the current running app. """
return self.cast.app_id
@property
def app_name(self):
""" Name of the current running app. """
return self.cast.app_display_name
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return SUPPORT_CAST
def turn_on(self):
""" Turns on the ChromeCast. """
# The only way we can turn the Chromecast is on is by launching an app
if not self.cast.status or not self.cast.status.is_active_input:
if self.cast.app_id:
self.cast.quit_app()
self.cast.play_media(
CAST_SPLASH, cast.STREAM_TYPE_BUFFERED)
def turn_off(self):
""" Turns Chromecast off. """
self.cast.quit_app()
def mute_volume(self, mute):
""" mute the volume. """
self.cast.set_volume_muted(mute)
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
self.cast.set_volume(volume)
def media_play(self):
""" Send play commmand. """
self.cast.media_controller.play()
def media_pause(self):
""" Send pause command. """
self.cast.media_controller.pause()
def media_previous_track(self):
""" Send previous track command. """
self.cast.media_controller.rewind()
def media_next_track(self):
""" Send next track command. """
self.cast.media_controller.skip()
def media_seek(self, position):
""" Seek the media to a specific location. """
self.cast.media_controller.seek(position)
def play_youtube(self, media_id):
""" Plays a YouTube media. """
self.youtube.play_video(media_id)
# implementation of chromecast status_listener methods
def new_cast_status(self, status):
""" Called when a new cast status is received. """
self.cast_status = status
self.update_ha_state()
def new_media_status(self, status):
""" Called when a new media status is received. """
self.media_status = status
self.update_ha_state()

View File

@@ -0,0 +1,338 @@
"""
homeassistant.components.media_player.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Demo implementation of the media player.
"""
from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
from homeassistant.components.media_player import (
MediaPlayerDevice, YOUTUBE_COVER_URL_FORMAT,
MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_YOUTUBE,
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PREVIOUS_TRACK,
SUPPORT_NEXT_TRACK)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the cast platform. """
add_devices([
DemoYoutubePlayer(
'Living Room', 'eyU3bRy2x44',
'♥♥ The Best Fireplace Video (3 hours)'),
DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours'),
DemoMusicPlayer(), DemoTVShowPlayer(),
])
YOUTUBE_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_YOUTUBE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
MUSIC_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
NETFLIX_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
class AbstractDemoPlayer(MediaPlayerDevice):
""" Base class for demo media players. """
# We only implement the methods that we support
# pylint: disable=abstract-method
def __init__(self, name):
self._name = name
self._player_state = STATE_PLAYING
self._volume_level = 1.0
self._volume_muted = False
@property
def should_poll(self):
""" We will push an update after each command. """
return False
@property
def name(self):
""" Name of the media player. """
return self._name
@property
def state(self):
""" State of the player. """
return self._player_state
@property
def volume_level(self):
""" Volume level of the media player (0..1). """
return self._volume_level
@property
def is_volume_muted(self):
""" Boolean if volume is currently muted. """
return self._volume_muted
def turn_on(self):
""" turn the media player on. """
self._player_state = STATE_PLAYING
self.update_ha_state()
def turn_off(self):
""" turn the media player off. """
self._player_state = STATE_OFF
self.update_ha_state()
def mute_volume(self, mute):
""" mute the volume. """
self._volume_muted = mute
self.update_ha_state()
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
self._volume_level = volume
self.update_ha_state()
def media_play(self):
""" Send play commmand. """
self._player_state = STATE_PLAYING
self.update_ha_state()
def media_pause(self):
""" Send pause command. """
self._player_state = STATE_PAUSED
self.update_ha_state()
class DemoYoutubePlayer(AbstractDemoPlayer):
""" A Demo media player that only supports YouTube. """
# We only implement the methods that we support
# pylint: disable=abstract-method
def __init__(self, name, youtube_id=None, media_title=None):
super().__init__(name)
self.youtube_id = youtube_id
self._media_title = media_title
@property
def media_content_id(self):
""" Content ID of current playing media. """
return self.youtube_id
@property
def media_content_type(self):
""" Content type of current playing media. """
return MEDIA_TYPE_VIDEO
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return 360
@property
def media_image_url(self):
""" Image url of current playing media. """
return YOUTUBE_COVER_URL_FORMAT.format(self.youtube_id)
@property
def media_title(self):
""" Title of current playing media. """
return self._media_title
@property
def app_name(self):
""" Current running app. """
return "YouTube"
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return YOUTUBE_PLAYER_SUPPORT
def play_youtube(self, media_id):
""" Plays a YouTube media. """
self.youtube_id = media_id
self._media_title = 'some YouTube video'
self.update_ha_state()
class DemoMusicPlayer(AbstractDemoPlayer):
""" A Demo media player that only supports YouTube. """
# We only implement the methods that we support
# pylint: disable=abstract-method
tracks = [
('Technohead', 'I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)'),
('Paul Elstak', 'Luv U More'),
('Dune', 'Hardcore Vibes'),
('Nakatomi', 'Children Of The Night'),
('Party Animals',
'Have You Ever Been Mellow? (Flamman & Abraxas Radio Mix)'),
('Rob G.*', 'Ecstasy, You Got What I Need'),
('Lipstick', "I'm A Raver"),
('4 Tune Fairytales', 'My Little Fantasy (Radio Edit)'),
('Prophet', "The Big Boys Don't Cry"),
('Lovechild', 'All Out Of Love (DJ Weirdo & Sim Remix)'),
('Stingray & Sonic Driver', 'Cold As Ice (El Bruto Remix)'),
('Highlander', 'Hold Me Now (Bass-D & King Matthew Remix)'),
('Juggernaut', 'Ruffneck Rules Da Artcore Scene (12" Edit)'),
('Diss Reaction', 'Jiiieehaaaa '),
('Flamman And Abraxas', 'Good To Go (Radio Mix)'),
('Critical Mass', 'Dancing Together'),
('Charly Lownoise & Mental Theo',
'Ultimate Sex Track (Bass-D & King Matthew Remix)'),
]
def __init__(self):
super().__init__('Walkman')
self._cur_track = 0
@property
def media_content_id(self):
""" Content ID of current playing media. """
return 'bounzz-1'
@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. """
return 213
@property
def media_image_url(self):
""" Image url of current playing media. """
return 'https://graph.facebook.com/107771475912710/picture'
@property
def media_title(self):
""" Title of current playing media. """
return self.tracks[self._cur_track][1]
@property
def media_artist(self):
""" Artist of current playing media. (Music track only) """
return self.tracks[self._cur_track][0]
@property
def media_album_name(self):
""" Album of current playing media. (Music track only) """
# pylint: disable=no-self-use
return "Bounzz"
@property
def media_track(self):
""" Track number of current playing media. (Music track only) """
return self._cur_track + 1
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
support = MUSIC_PLAYER_SUPPORT
if self._cur_track > 1:
support |= SUPPORT_PREVIOUS_TRACK
if self._cur_track < len(self.tracks)-1:
support |= SUPPORT_NEXT_TRACK
return support
def media_previous_track(self):
""" Send previous track command. """
if self._cur_track > 0:
self._cur_track -= 1
self.update_ha_state()
def media_next_track(self):
""" Send next track command. """
if self._cur_track < len(self.tracks)-1:
self._cur_track += 1
self.update_ha_state()
class DemoTVShowPlayer(AbstractDemoPlayer):
""" A Demo media player that only supports YouTube. """
# We only implement the methods that we support
# pylint: disable=abstract-method
def __init__(self):
super().__init__('Lounge room')
self._cur_episode = 1
self._episode_count = 13
@property
def media_content_id(self):
""" Content ID of current playing media. """
return 'house-of-cards-1'
@property
def media_content_type(self):
""" Content type of current playing media. """
return MEDIA_TYPE_TVSHOW
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return 3600
@property
def media_image_url(self):
""" Image url of current playing media. """
return 'https://graph.facebook.com/HouseofCards/picture'
@property
def media_title(self):
""" Title of current playing media. """
return 'Chapter {}'.format(self._cur_episode)
@property
def media_series_title(self):
""" Series title of current playing media. (TV Show only)"""
return 'House of Cards'
@property
def media_season(self):
""" Season of current playing media. (TV Show only) """
return 1
@property
def media_episode(self):
""" Episode of current playing media. (TV Show only) """
return self._cur_episode
@property
def app_name(self):
""" Current running app. """
return "Netflix"
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
support = NETFLIX_PLAYER_SUPPORT
if self._cur_episode > 1:
support |= SUPPORT_PREVIOUS_TRACK
if self._cur_episode < self._episode_count:
support |= SUPPORT_NEXT_TRACK
return support
def media_previous_track(self):
""" Send previous track command. """
if self._cur_episode > 1:
self._cur_episode -= 1
self.update_ha_state()
def media_next_track(self):
""" Send next track command. """
if self._cur_episode < self._episode_count:
self._cur_episode += 1
self.update_ha_state()

View File

@@ -0,0 +1,306 @@
"""
homeassistant.components.media_player.kodi
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides an interface to the XBMC/Kodi JSON-RPC API
Configuration:
To use the Kodi you will need to add something like the following to
your config/configuration.yaml.
media_player:
platform: kodi
name: Kodi
url: http://192.168.0.123/jsonrpc
user: kodi
password: my_secure_password
Variables:
name
*Optional
The name of the device.
url
*Required
The URL of the XBMC/Kodi JSON-RPC API. Example: http://192.168.0.123/jsonrpc
user
*Optional
The XBMC/Kodi HTTP username.
password
*Optional
The XBMC/Kodi HTTP password.
"""
import urllib
import logging
from homeassistant.components.media_player import (
MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK)
from homeassistant.const import (
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF)
try:
import jsonrpc_requests
except ImportError:
jsonrpc_requests = None
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['jsonrpc-requests==0.1']
SUPPORT_KODI = 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 kodi platform. """
global jsonrpc_requests # pylint: disable=invalid-name
if jsonrpc_requests is None:
import jsonrpc_requests as jsonrpc_requests_
jsonrpc_requests = jsonrpc_requests_
add_devices([
KodiDevice(
config.get('name', 'Kodi'),
config.get('url'),
auth=(
config.get('user', ''),
config.get('password', ''))),
])
def _get_image_url(kodi_url):
""" Helper function that parses the thumbnail URLs used by Kodi """
url_components = urllib.parse.urlparse(kodi_url)
if url_components.scheme == 'image':
return urllib.parse.unquote(url_components.netloc)
class KodiDevice(MediaPlayerDevice):
""" Represents a XBMC/Kodi device. """
# pylint: disable=too-many-public-methods
def __init__(self, name, url, auth=None):
self._name = name
self._url = url
self._server = jsonrpc_requests.Server(url, auth=auth)
self._players = None
self._properties = None
self._item = None
self._app_properties = None
self.update()
@property
def name(self):
""" Returns the name of the device. """
return self._name
def _get_players(self):
""" Returns the active player objects or None """
try:
return self._server.Player.GetActivePlayers()
except jsonrpc_requests.jsonrpc.TransportError:
return None
@property
def state(self):
""" Returns the state of the device. """
if self._players is None:
return STATE_OFF
if len(self._players) == 0:
return STATE_IDLE
if self._properties['speed'] == 0:
return STATE_PAUSED
else:
return STATE_PLAYING
def update(self):
""" Retrieve latest state. """
self._players = self._get_players()
if self._players is not None and len(self._players) > 0:
player_id = self._players[0]['playerid']
assert isinstance(player_id, int)
self._properties = self._server.Player.GetProperties(
player_id,
['time', 'totaltime', 'speed']
)
self._item = self._server.Player.GetItem(
player_id,
['title', 'file', 'uniqueid', 'thumbnail', 'artist']
)['item']
self._app_properties = self._server.Application.GetProperties(
['volume', 'muted']
)
else:
self._properties = None
self._item = None
self._app_properties = None
@property
def volume_level(self):
""" Volume level of the media player (0..1). """
if self._app_properties is not None:
return self._app_properties['volume'] / 100.0
@property
def is_volume_muted(self):
""" Boolean if volume is currently muted. """
if self._app_properties is not None:
return self._app_properties['muted']
@property
def media_content_id(self):
""" Content ID of current playing media. """
if self._item is not None:
return self._item['uniqueid']
@property
def media_content_type(self):
""" Content type of current playing media. """
if self._players is not None and len(self._players) > 0:
return self._players[0]['type']
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
if self._properties is not None:
total_time = self._properties['totaltime']
return (
total_time['hours'] * 3600 +
total_time['minutes'] * 60 +
total_time['seconds'])
@property
def media_image_url(self):
""" Image url of current playing media. """
if self._item is not None:
return _get_image_url(self._item['thumbnail'])
@property
def media_title(self):
""" Title of current playing media. """
# find a string we can use as a title
if self._item is not None:
return self._item.get(
'title',
self._item.get(
'label',
self._item.get(
'file',
'unknown')))
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return SUPPORT_KODI
def turn_off(self):
""" turn_off media player. """
self._server.System.Shutdown()
self.update_ha_state()
def volume_up(self):
""" volume_up media player. """
assert self._server.Input.ExecuteAction('volumeup') == 'OK'
self.update_ha_state()
def volume_down(self):
""" volume_down media player. """
assert self._server.Input.ExecuteAction('volumedown') == 'OK'
self.update_ha_state()
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
self._server.Application.SetVolume(int(volume * 100))
self.update_ha_state()
def mute_volume(self, mute):
""" mute (true) or unmute (false) media player. """
self._server.Application.SetMute(mute)
self.update_ha_state()
def _set_play_state(self, state):
""" Helper method for play/pause/toggle """
players = self._get_players()
if len(players) != 0:
self._server.Player.PlayPause(players[0]['playerid'], state)
self.update_ha_state()
def media_play_pause(self):
""" media_play_pause media player. """
self._set_play_state('toggle')
def media_play(self):
""" media_play media player. """
self._set_play_state(True)
def media_pause(self):
""" media_pause media player. """
self._set_play_state(False)
def _goto(self, direction):
""" Helper method used for previous/next track """
players = self._get_players()
if len(players) != 0:
self._server.Player.GoTo(players[0]['playerid'], direction)
self.update_ha_state()
def media_next_track(self):
""" Send next track command. """
self._goto('next')
def media_previous_track(self):
""" Send next track command. """
# first seek to position 0, Kodi seems to go to the beginning
# of the current track current track is not at the beginning
self.media_seek(0)
self._goto('previous')
def media_seek(self, position):
""" Send seek command. """
players = self._get_players()
time = {}
time['milliseconds'] = int((position % 1) * 1000)
position = int(position)
time['seconds'] = int(position % 60)
position /= 60
time['minutes'] = int(position % 60)
position /= 60
time['hours'] = int(position)
if len(players) != 0:
self._server.Player.Seek(players[0]['playerid'], time)
self.update_ha_state()
def turn_on(self):
""" turn the media player on. """
raise NotImplementedError()
def play_youtube(self, media_id):
""" Plays a YouTube media. """
raise NotImplementedError()

View File

@@ -0,0 +1,204 @@
"""
homeassistant.components.media_player.mpd
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to interact with a Music Player Daemon.
Configuration:
To use MPD you will need to add something like the following to your
config/configuration.yaml
media_player:
platform: mpd
server: 127.0.0.1
port: 6600
location: bedroom
Variables:
server
*Required
IP address of the Music Player Daemon. Example: 192.168.1.32
port
*Optional
Port of the Music Player Daemon, defaults to 6600. Example: 6600
location
*Optional
Location of your Music Player Daemon.
"""
import logging
import socket
try:
import mpd
except ImportError:
mpd = None
from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
from homeassistant.components.media_player import (
MediaPlayerDevice,
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_TURN_OFF,
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
MEDIA_TYPE_MUSIC)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['python-mpd2==0.5.4']
SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the MPD platform. """
daemon = config.get('server', None)
port = config.get('port', 6600)
location = config.get('location', 'MPD')
global mpd # pylint: disable=invalid-name
if mpd is None:
import mpd as mpd_
mpd = mpd_
# pylint: disable=no-member
try:
mpd_client = mpd.MPDClient()
mpd_client.connect(daemon, port)
mpd_client.close()
mpd_client.disconnect()
except socket.error:
_LOGGER.error(
"Unable to connect to MPD. "
"Please check your settings")
return False
add_devices([MpdDevice(daemon, port, location)])
class MpdDevice(MediaPlayerDevice):
""" Represents a MPD server. """
# MPD confuses pylint
# pylint: disable=no-member, abstract-method
def __init__(self, server, port, location):
self.server = server
self.port = port
self._name = location
self.status = None
self.currentsong = None
self.client = mpd.MPDClient()
self.client.timeout = 10
self.client.idletimeout = None
self.update()
def update(self):
try:
self.status = self.client.status()
self.currentsong = self.client.currentsong()
except mpd.ConnectionError:
self.client.connect(self.server, self.port)
self.status = self.client.status()
self.currentsong = self.client.currentsong()
@property
def name(self):
""" Returns the name of the device. """
return self._name
@property
def state(self):
""" Returns the media state. """
if self.status['state'] == 'play':
return STATE_PLAYING
elif self.status['state'] == 'pause':
return STATE_PAUSED
else:
return STATE_OFF
@property
def media_content_id(self):
""" Content ID of current playing media. """
return self.currentsong['id']
@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. """
# Time does not exist for streams
return self.currentsong.get('time')
@property
def media_title(self):
""" Title of current playing media. """
return self.currentsong['title']
@property
def media_artist(self):
""" Artist of current playing media. (Music track only) """
return self.currentsong.get('artist')
@property
def media_album_name(self):
""" Album of current playing media. (Music track only) """
return self.currentsong.get('album')
@property
def volume_level(self):
return int(self.status['volume'])/100
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return SUPPORT_MPD
def turn_off(self):
""" Service to exit the running MPD. """
self.client.stop()
def set_volume_level(self, volume):
""" Sets volume """
self.client.setvol(int(volume * 100))
def volume_up(self):
""" Service to send the MPD the command for volume up. """
current_volume = int(self.status['volume'])
if current_volume <= 100:
self.client.setvol(current_volume + 5)
def volume_down(self):
""" Service to send the MPD the command for volume down. """
current_volume = int(self.status['volume'])
if current_volume >= 0:
self.client.setvol(current_volume - 5)
def media_play(self):
""" Service to send the MPD the command for play/pause. """
self.client.start()
def media_pause(self):
""" Service to send the MPD the command for play/pause. """
self.client.pause()
def media_next_track(self):
""" Service to send the MPD the command for next track. """
self.client.next()
def media_previous_track(self):
""" Service to send the MPD the command for previous track. """
self.client.previous()

View File

@@ -0,0 +1,319 @@
"""
homeassistant.components.media_player.squeezebox
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides an interface to the Logitech SqueezeBox API
Configuration:
To use SqueezeBox add something like this to your configuration:
media_player:
platform: squeezebox
host: 192.168.1.21
port: 9090
username: user
password: password
Variables:
host
*Required
The host name or address of the Logitech Media Server
port
*Optional
Telnet port to Logitech Media Server, default 9090
usermame
*Optional
Username, if password protection is enabled
password
*Optional
Password, if password protection is enabled
"""
import logging
import telnetlib
import urllib.parse
from homeassistant.components.media_player import (
MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
MEDIA_TYPE_MUSIC, DOMAIN)
from homeassistant.const import (
CONF_HOST, CONF_USERNAME, CONF_PASSWORD,
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_UNKNOWN)
_LOGGER = logging.getLogger(__name__)
SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK |\
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the squeezebox platform. """
if not config.get(CONF_HOST):
_LOGGER.error(
"Missing required configuration items in %s: %s",
DOMAIN,
CONF_HOST)
return False
lms = LogitechMediaServer(
config.get(CONF_HOST),
config.get('port', '9090'),
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD))
if not lms.init_success:
return False
add_devices(lms.create_players())
return True
class LogitechMediaServer(object):
""" Represents a Logitech media server. """
def __init__(self, host, port, username, password):
self.host = host
self.port = port
self._username = username
self._password = password
self.http_port = self._get_http_port()
self.init_success = True if self.http_port else False
def _get_http_port(self):
""" Get http port from media server, it is used to get cover art """
http_port = None
try:
http_port = self.query('pref', 'httpport', '?')
if not http_port:
_LOGGER.error(
"Unable to read data from server %s:%s",
self.host,
self.port)
return
return http_port
except ConnectionError as ex:
_LOGGER.error(
"Failed to connect to server %s:%s - %s",
self.host,
self.port,
ex)
return
def create_players(self):
""" Create a list of SqueezeBoxDevices connected to the LMS """
players = []
count = self.query('player', 'count', '?')
for index in range(0, int(count)):
player_id = self.query('player', 'id', str(index), '?')
player = SqueezeBoxDevice(self, player_id)
players.append(player)
return players
def query(self, *parameters):
""" Send request and await response from server """
telnet = telnetlib.Telnet(self.host, self.port)
if self._username and self._password:
telnet.write('login {username} {password}\n'.format(
username=self._username,
password=self._password).encode('UTF-8'))
telnet.read_until(b'\n', timeout=3)
message = '{}\n'.format(' '.join(parameters))
telnet.write(message.encode('UTF-8'))
response = telnet.read_until(b'\n', timeout=3)\
.decode('UTF-8')\
.split(' ')[-1]\
.strip()
telnet.write(b'exit\n')
return urllib.parse.unquote(response)
def get_player_status(self, player):
""" Get ithe status of a player """
# (title) : Song title
# Requested Information
# a (artist): Artist name 'artist'
# d (duration): Song duration in seconds 'duration'
# K (artwork_url): URL to remote artwork
tags = 'adK'
new_status = {}
telnet = telnetlib.Telnet(self.host, self.port)
telnet.write('{player} status - 1 tags:{tags}\n'.format(
player=player,
tags=tags
).encode('UTF-8'))
response = telnet.read_until(b'\n', timeout=3)\
.decode('UTF-8')\
.split(' ')
telnet.write(b'exit\n')
for item in response:
parts = urllib.parse.unquote(item).partition(':')
new_status[parts[0]] = parts[2]
return new_status
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-public-methods
class SqueezeBoxDevice(MediaPlayerDevice):
""" Represents a SqueezeBox device. """
# pylint: disable=too-many-arguments
def __init__(self, lms, player_id):
super(SqueezeBoxDevice, self).__init__()
self._lms = lms
self._id = player_id
self._name = self._lms.query(self._id, 'name', '?')
self._status = self._lms.get_player_status(self._id)
@property
def name(self):
""" Returns the name of the device. """
return self._name
@property
def state(self):
""" Returns the state of the device. """
if 'power' in self._status and self._status['power'] == '0':
return STATE_OFF
if 'mode' in self._status:
if self._status['mode'] == 'pause':
return STATE_PAUSED
if self._status['mode'] == 'play':
return STATE_PLAYING
if self._status['mode'] == 'stop':
return STATE_IDLE
return STATE_UNKNOWN
def update(self):
""" Retrieve latest state. """
self._status = self._lms.get_player_status(self._name)
@property
def volume_level(self):
""" Volume level of the media player (0..1). """
if 'mixer volume' in self._status:
return int(self._status['mixer volume']) / 100.0
@property
def is_volume_muted(self):
if 'mixer volume' in self._status:
return self._status['mixer volume'].startswith('-')
@property
def media_content_id(self):
""" Content ID of current playing media. """
if 'current_title' in self._status:
return self._status['current_title']
@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. """
if 'duration' in self._status:
return int(float(self._status['duration']))
@property
def media_image_url(self):
""" Image url of current playing media. """
if 'artwork_url' in self._status:
return self._status['artwork_url']
return 'http://{server}:{port}/music/current/cover.jpg?player={player}'\
.format(
server=self._lms.host,
port=self._lms.http_port,
player=self._id)
@property
def media_title(self):
""" Title of current playing media. """
if 'artist' in self._status and 'title' in self._status:
return '{artist} - {title}'.format(
artist=self._status['artist'],
title=self._status['title']
)
if 'current_title' in self._status:
return self._status['current_title']
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return SUPPORT_SQUEEZEBOX
def turn_off(self):
""" turn_off media player. """
self._lms.query(self._id, 'power', '0')
self.update_ha_state()
def volume_up(self):
""" volume_up media player. """
self._lms.query(self._id, 'mixer', 'volume', '+5')
self.update_ha_state()
def volume_down(self):
""" volume_down media player. """
self._lms.query(self._id, 'mixer', 'volume', '-5')
self.update_ha_state()
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
volume_percent = str(int(volume*100))
self._lms.query(self._id, 'mixer', 'volume', volume_percent)
self.update_ha_state()
def mute_volume(self, mute):
""" mute (true) or unmute (false) media player. """
mute_numeric = '1' if mute else '0'
self._lms.query(self._id, 'mixer', 'muting', mute_numeric)
self.update_ha_state()
def media_play_pause(self):
""" media_play_pause media player. """
self._lms.query(self._id, 'pause')
self.update_ha_state()
def media_play(self):
""" media_play media player. """
self._lms.query(self._id, 'play')
self.update_ha_state()
def media_pause(self):
""" media_pause media player. """
self._lms.query(self._id, 'pause', '0')
self.update_ha_state()
def media_next_track(self):
""" Send next track command. """
self._lms.query(self._id, 'playlist', 'index', '+1')
self.update_ha_state()
def media_previous_track(self):
""" Send next track command. """
self._lms.query(self._id, 'playlist', 'index', '-1')
self.update_ha_state()
def media_seek(self, position):
""" Send seek command. """
self._lms.query(self._id, 'time', position)
self.update_ha_state()
def turn_on(self):
""" turn the media player on. """
self._lms.query(self._id, 'power', '1')
self.update_ha_state()
def play_youtube(self, media_id):
""" Plays a YouTube media. """
raise NotImplementedError()

View File

@@ -0,0 +1,108 @@
"""
homeassistant.components.modbus
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Modbus component, using pymodbus (python3 branch)
Configuration:
To use the Forecast sensor you will need to add something like the
following to your config/configuration.yaml
Configuration:
To use the Modbus component you will need to add something like the following
to your config/configuration.yaml
#Modbus TCP
modbus:
type: tcp
host: 127.0.0.1
port: 2020
#Modbus RTU
modbus:
type: serial
method: rtu
port: /dev/ttyUSB0
baudrate: 9600
stopbits: 1
bytesize: 8
parity: N
"""
import logging
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP)
DOMAIN = "modbus"
DEPENDENCIES = []
REQUIREMENTS = ['https://github.com/bashwork/pymodbus/archive/' +
'd7fc4f1cc975631e0a9011390e8017f64b612661.zip']
# Type of network
MEDIUM = "type"
# if MEDIUM == "serial"
METHOD = "method"
SERIAL_PORT = "port"
BAUDRATE = "baudrate"
STOPBITS = "stopbits"
BYTESIZE = "bytesize"
PARITY = "parity"
# if MEDIUM == "tcp" or "udp"
HOST = "host"
IP_PORT = "port"
_LOGGER = logging.getLogger(__name__)
NETWORK = None
TYPE = None
def setup(hass, config):
""" Setup Modbus component. """
# Modbus connection type
# pylint: disable=global-statement, import-error
global TYPE
TYPE = config[DOMAIN][MEDIUM]
# Connect to Modbus network
# pylint: disable=global-statement, import-error
global NETWORK
if TYPE == "serial":
from pymodbus.client.sync import ModbusSerialClient as ModbusClient
NETWORK = ModbusClient(method=config[DOMAIN][METHOD],
port=config[DOMAIN][SERIAL_PORT],
baudrate=config[DOMAIN][BAUDRATE],
stopbits=config[DOMAIN][STOPBITS],
bytesize=config[DOMAIN][BYTESIZE],
parity=config[DOMAIN][PARITY])
elif TYPE == "tcp":
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
NETWORK = ModbusClient(host=config[DOMAIN][HOST],
port=config[DOMAIN][IP_PORT])
elif TYPE == "udp":
from pymodbus.client.sync import ModbusUdpClient as ModbusClient
NETWORK = ModbusClient(host=config[DOMAIN][HOST],
port=config[DOMAIN][IP_PORT])
else:
return False
def stop_modbus(event):
""" Stop Modbus service. """
NETWORK.close()
def start_modbus(event):
""" Start Modbus service. """
NETWORK.connect()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus)
# Tells the bootstrapper that the component was successfully initialized
return True

View File

@@ -0,0 +1,256 @@
"""
homeassistant.components.mqtt
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
MQTT component, using paho-mqtt. This component needs a MQTT broker like
Mosquitto or Mosca. The Eclipse Foundation is running a public MQTT server
at iot.eclipse.org. If you prefer to use that one, keep in mind to adjust
the topic/client ID and that your messages are public.
Configuration:
To use MQTT you will need to add something like the following to your
config/configuration.yaml.
mqtt:
broker: 127.0.0.1
Or, if you want more options:
mqtt:
broker: 127.0.0.1
port: 1883
client_id: home-assistant-1
keepalive: 60
username: your_username
password: your_secret_password
Variables:
broker
*Required
This is the IP address of your MQTT broker, e.g. 192.168.1.32.
port
*Optional
The network port to connect to. Default is 1883.
client_id
*Optional
Client ID that Home Assistant will use. Has to be unique on the server.
Default is a random generated one.
keepalive
*Optional
The keep alive in seconds for this client. Default is 60.
"""
import logging
import socket
from homeassistant.exceptions import HomeAssistantError
import homeassistant.util as util
from homeassistant.helpers import validate_config
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
_LOGGER = logging.getLogger(__name__)
DOMAIN = "mqtt"
MQTT_CLIENT = None
DEFAULT_PORT = 1883
DEFAULT_KEEPALIVE = 60
DEFAULT_QOS = 0
SERVICE_PUBLISH = 'publish'
EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED'
DEPENDENCIES = []
REQUIREMENTS = ['paho-mqtt==1.1']
CONF_BROKER = 'broker'
CONF_PORT = 'port'
CONF_CLIENT_ID = 'client_id'
CONF_KEEPALIVE = 'keepalive'
CONF_USERNAME = 'username'
CONF_PASSWORD = 'password'
ATTR_QOS = 'qos'
ATTR_TOPIC = 'topic'
ATTR_PAYLOAD = 'payload'
def publish(hass, topic, payload):
""" Send an MQTT message. """
data = {
ATTR_TOPIC: topic,
ATTR_PAYLOAD: payload,
}
hass.services.call(DOMAIN, SERVICE_PUBLISH, data)
def subscribe(hass, topic, callback, qos=0):
""" Subscribe to a topic. """
def mqtt_topic_subscriber(event):
""" Match subscribed MQTT topic. """
if _match_topic(topic, event.data[ATTR_TOPIC]):
callback(event.data[ATTR_TOPIC], event.data[ATTR_PAYLOAD],
event.data[ATTR_QOS])
hass.bus.listen(EVENT_MQTT_MESSAGE_RECEIVED, mqtt_topic_subscriber)
if topic not in MQTT_CLIENT.topics:
MQTT_CLIENT.subscribe(topic, qos)
def setup(hass, config):
""" Get the MQTT protocol service. """
if not validate_config(config, {DOMAIN: ['broker']}, _LOGGER):
return False
conf = config[DOMAIN]
broker = conf[CONF_BROKER]
port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT)
client_id = util.convert(conf.get(CONF_CLIENT_ID), str)
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)
global MQTT_CLIENT
try:
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, username,
password)
except socket.error:
_LOGGER.exception("Can't connect to the broker. "
"Please check your settings and the broker "
"itself.")
return False
def stop_mqtt(event):
""" Stop MQTT component. """
MQTT_CLIENT.stop()
def start_mqtt(event):
""" Launch MQTT component when Home Assistant starts up. """
MQTT_CLIENT.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_mqtt)
def publish_service(call):
""" Handle MQTT publish service calls. """
msg_topic = call.data.get(ATTR_TOPIC)
payload = call.data.get(ATTR_PAYLOAD)
if msg_topic is None or payload is None:
return
MQTT_CLIENT.publish(msg_topic, payload)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_mqtt)
hass.services.register(DOMAIN, SERVICE_PUBLISH, publish_service)
return True
# This is based on one of the paho-mqtt examples:
# http://git.eclipse.org/c/paho/org.eclipse.paho.mqtt.python.git/tree/examples/sub-class.py
# pylint: disable=too-many-arguments
class MQTT(object): # pragma: no cover
""" Implements messaging service for MQTT. """
def __init__(self, hass, broker, port, client_id, keepalive, username,
password):
import paho.mqtt.client as mqtt
self.hass = hass
self._progress = {}
self.topics = {}
if client_id is None:
self._mqttc = mqtt.Client()
else:
self._mqttc = mqtt.Client(client_id)
if username is not None:
self._mqttc.username_pw_set(username, password)
self._mqttc.on_subscribe = self._mqtt_on_subscribe
self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe
self._mqttc.on_connect = self._mqtt_on_connect
self._mqttc.on_message = self._mqtt_on_message
self._mqttc.connect(broker, port, keepalive)
def publish(self, topic, payload):
""" Publish a MQTT message. """
self._mqttc.publish(topic, payload)
def unsubscribe(self, topic):
""" Unsubscribe from topic. """
result, mid = self._mqttc.unsubscribe(topic)
_raise_on_error(result)
self._progress[mid] = topic
def start(self):
""" Run the MQTT client. """
self._mqttc.loop_start()
def stop(self):
""" Stop the MQTT client. """
self._mqttc.loop_stop()
def subscribe(self, topic, qos):
""" Subscribe to a topic. """
if topic in self.topics:
return
result, mid = self._mqttc.subscribe(topic, qos)
_raise_on_error(result)
self._progress[mid] = topic
self.topics[topic] = None
def _mqtt_on_connect(self, mqttc, obj, flags, result_code):
""" On connect, resubscribe to all topics we were subscribed to. """
old_topics = self.topics
self._progress = {}
self.topics = {}
for topic, qos in old_topics.items():
# qos is None if we were in process of subscribing
if qos is not None:
self._mqttc.subscribe(topic, qos)
def _mqtt_on_subscribe(self, mqttc, obj, mid, granted_qos):
""" Called when subscribe succesfull. """
topic = self._progress.pop(mid, None)
if topic is None:
return
self.topics[topic] = granted_qos
def _mqtt_on_unsubscribe(self, mqttc, obj, mid, granted_qos):
""" Called when subscribe succesfull. """
topic = self._progress.pop(mid, None)
if topic is None:
return
self.topics.pop(topic, None)
def _mqtt_on_message(self, mqttc, obj, msg):
""" Message callback """
self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, {
ATTR_TOPIC: msg.topic,
ATTR_QOS: msg.qos,
ATTR_PAYLOAD: msg.payload.decode('utf-8'),
})
def _raise_on_error(result): # pragma: no cover
""" Raise error if error result. """
if result != 0:
raise HomeAssistantError('Error talking to MQTT: {}'.format(result))
def _match_topic(subscription, topic):
""" Returns if topic matches subscription. """
if subscription.endswith('#'):
return (subscription[:-2] == topic or
topic.startswith(subscription[:-1]))
sub_parts = subscription.split('/')
topic_parts = topic.split('/')
return (len(sub_parts) == len(topic_parts) and
all(a == b for a, b in zip(sub_parts, topic_parts) if a != '+'))

View File

@@ -0,0 +1,87 @@
"""
homeassistant.components.notify
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to notify people.
"""
from functools import partial
import logging
from homeassistant.loader import get_component
from homeassistant.helpers import config_per_platform
from homeassistant.const import CONF_NAME
DOMAIN = "notify"
DEPENDENCIES = []
# Title of notification
ATTR_TITLE = "title"
ATTR_TITLE_DEFAULT = "Home Assistant"
# Text to notify user of
ATTR_MESSAGE = "message"
SERVICE_NOTIFY = "notify"
_LOGGER = logging.getLogger(__name__)
def send_message(hass, message):
""" Send a notification message. """
hass.services.call(DOMAIN, SERVICE_NOTIFY, {ATTR_MESSAGE: message})
def setup(hass, config):
""" Sets up notify services. """
success = False
for platform, p_config in config_per_platform(config, DOMAIN, _LOGGER):
# get platform
notify_implementation = get_component(
'notify.{}'.format(platform))
if notify_implementation is None:
_LOGGER.error("Unknown notification service specified.")
continue
# create platform service
notify_service = notify_implementation.get_service(
hass, {DOMAIN: p_config})
if notify_service is None:
_LOGGER.error("Failed to initialize notification service %s",
platform)
continue
# create service handler
def notify_message(notify_service, call):
""" Handle sending notification message service calls. """
message = call.data.get(ATTR_MESSAGE)
if message is None:
return
title = call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
notify_service.send_message(message, title=title)
# 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)
success = True
return success
# pylint: disable=too-few-public-methods
class BaseNotificationService(object):
""" Provides an ABC for notification services. """
def send_message(self, message, **kwargs):
"""
Send a message.
kwargs can contain ATTR_TITLE to specify a title.
"""
raise NotImplementedError

View File

@@ -0,0 +1,31 @@
"""
homeassistant.components.notify.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Demo notification service.
"""
from homeassistant.components.notify import ATTR_TITLE, BaseNotificationService
EVENT_NOTIFY = "notify"
def get_service(hass, config):
""" Get the demo notification service. """
return DemoNotificationService(hass)
# pylint: disable=too-few-public-methods
class DemoNotificationService(BaseNotificationService):
""" Implements demo notification service. """
def __init__(self, hass):
self.hass = hass
def send_message(self, message="", **kwargs):
""" Send a message to a user. """
title = kwargs.get(ATTR_TITLE)
self.hass.bus.fire(EVENT_NOTIFY, {"title": title, "message": message})

View File

@@ -0,0 +1,78 @@
"""
homeassistant.components.notify.file
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
File notification service.
Configuration:
To use the File notifier you will need to add something like the following
to your config/configuration.yaml
notify:
platform: file
filename: FILENAME
timestamp: 1 or 0
Variables:
filename
*Required
Name of the file to use. The file will be created if it doesn't exist and saved
in your config/ folder.
timestamp
*Required
Add a timestamp to the entry, valid entries are 1 or 0.
"""
import logging
import os
import homeassistant.util.dt as dt_util
from homeassistant.helpers import validate_config
from homeassistant.components.notify import (
DOMAIN, ATTR_TITLE, BaseNotificationService)
_LOGGER = logging.getLogger(__name__)
def get_service(hass, config):
""" Get the file notification service. """
if not validate_config(config,
{DOMAIN: ['filename',
'timestamp']},
_LOGGER):
return None
filename = config[DOMAIN]['filename']
timestamp = config[DOMAIN]['timestamp']
return FileNotificationService(hass, filename, timestamp)
# pylint: disable=too-few-public-methods
class FileNotificationService(BaseNotificationService):
""" Implements notification service for the File service. """
def __init__(self, hass, filename, add_timestamp):
self.filepath = os.path.join(hass.config.config_dir, filename)
self.add_timestamp = add_timestamp
def send_message(self, message="", **kwargs):
""" Send a message to a file. """
with open(self.filepath, 'a') as file:
if os.stat(self.filepath).st_size == 0:
title = '{} notifications (Log started: {})\n{}\n'.format(
kwargs.get(ATTR_TITLE),
dt_util.strip_microseconds(dt_util.utcnow()),
'-'*80)
file.write(title)
if self.add_timestamp == 1:
text = '{} {}\n'.format(dt_util.utcnow(), message)
file.write(text)
else:
text = '{}\n'.format(message)
file.write(text)

View File

@@ -0,0 +1,161 @@
"""
homeassistant.components.notify.instapush
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Instapush notification service.
Configuration:
To use the Instapush notifier you will need to add something like the following
to your config/configuration.yaml
notify:
platform: instapush
api_key: YOUR_APP_KEY
app_secret: YOUR_APP_SECRET
event: YOUR_EVENT
tracker: YOUR_TRACKER
VARIABLES:
api_key
*Required
To retrieve this value log into your account at https://instapush.im and go
to 'APPS', choose an app, and check 'Basic Info'.
app_secret
*Required
To get this value log into your account at https://instapush.im and go to
'APPS'. The 'Application ID' can be found under 'Basic Info'.
event
*Required
To retrieve a valid event log into your account at https://instapush.im and go
to 'APPS'. If you have no events to use with Home Assistant, create one event
for your app.
tracker
*Required
To retrieve the tracker value log into your account at https://instapush.im and
go to 'APPS', choose the app, and check the event entries.
Example usage of Instapush if you have an event 'notification' and a tracker
'home-assistant'.
curl -X POST \
-H "x-instapush-appid: YOUR_APP_KEY" \
-H "x-instapush-appsecret: YOUR_APP_SECRET" \
-H "Content-Type: application/json" \
-d '{"event":"notification","trackers":{"home-assistant":"Switch 1"}}' \
https://api.instapush.im/v1/post
Details for the API : https://instapush.im/developer/rest
"""
import logging
import json
from homeassistant.helpers import validate_config
from homeassistant.components.notify import (
DOMAIN, ATTR_TITLE, BaseNotificationService)
from homeassistant.const import CONF_API_KEY
_LOGGER = logging.getLogger(__name__)
_RESOURCE = 'https://api.instapush.im/v1/'
def get_service(hass, config):
""" Get the instapush notification service. """
if not validate_config(config,
{DOMAIN: [CONF_API_KEY,
'app_secret',
'event',
'tracker']},
_LOGGER):
return None
try:
import requests
except ImportError:
_LOGGER.exception(
"Unable to import requests. "
"Did you maybe not install the 'Requests' package?")
return None
# pylint: disable=unused-variable
try:
response = requests.get(_RESOURCE)
except requests.ConnectionError:
_LOGGER.error(
"Connection error "
"Please check if https://instapush.im is available.")
return None
instapush = requests.Session()
headers = {'x-instapush-appid': config[DOMAIN][CONF_API_KEY],
'x-instapush-appsecret': config[DOMAIN]['app_secret']}
response = instapush.get(_RESOURCE + 'events/list',
headers=headers)
try:
if response.json()['error']:
_LOGGER.error(response.json()['msg'])
# pylint: disable=bare-except
except:
try:
next(events for events in response.json()
if events['title'] == config[DOMAIN]['event'])
except StopIteration:
_LOGGER.error(
"No event match your given value. "
"Please create an event at https://instapush.im")
else:
return InstapushNotificationService(
config[DOMAIN].get(CONF_API_KEY),
config[DOMAIN]['app_secret'],
config[DOMAIN]['event'],
config[DOMAIN]['tracker']
)
# pylint: disable=too-few-public-methods
class InstapushNotificationService(BaseNotificationService):
""" Implements notification service for Instapush. """
def __init__(self, api_key, app_secret, event, tracker):
# pylint: disable=no-name-in-module, unused-variable
from requests import Session
self._api_key = api_key
self._app_secret = app_secret
self._event = event
self._tracker = tracker
self._headers = {
'x-instapush-appid': self._api_key,
'x-instapush-appsecret': self._app_secret,
'Content-Type': 'application/json'}
self.instapush = Session()
def send_message(self, message="", **kwargs):
""" Send a message to a user. """
title = kwargs.get(ATTR_TITLE)
data = {"event": self._event,
"trackers": {self._tracker: title + " : " + message}}
response = self.instapush.post(
_RESOURCE + 'post',
data=json.dumps(data),
headers=self._headers)
if response.json()['status'] == 401:
_LOGGER.error(
response.json()['msg'],
"Please check your details at https://instapush.im/")

View File

@@ -0,0 +1,97 @@
"""
homeassistant.components.notify.nma
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
NMA (Notify My Android) notification service.
Configuration:
To use the NMA notifier you will need to add something like the following
to your config/configuration.yaml
notify:
platform: nma
api_key: YOUR_API_KEY
VARIABLES:
api_key
*Required
Enter the API key for NMA. Go to https://www.notifymyandroid.com and create a
new API key to use with Home Assistant.
Details for the API : https://www.notifymyandroid.com/api.jsp
"""
import logging
import xml.etree.ElementTree as ET
from homeassistant.helpers import validate_config
from homeassistant.components.notify import (
DOMAIN, ATTR_TITLE, BaseNotificationService)
from homeassistant.const import CONF_API_KEY
_LOGGER = logging.getLogger(__name__)
_RESOURCE = 'https://www.notifymyandroid.com/publicapi/'
def get_service(hass, config):
""" Get the NMA notification service. """
if not validate_config(config,
{DOMAIN: [CONF_API_KEY]},
_LOGGER):
return None
try:
# pylint: disable=unused-variable
from requests import Session
except ImportError:
_LOGGER.exception(
"Unable to import requests. "
"Did you maybe not install the 'Requests' package?")
return None
nma = Session()
response = nma.get(_RESOURCE + 'verify',
params={"apikey": config[DOMAIN][CONF_API_KEY]})
tree = ET.fromstring(response.content)
if tree[0].tag == 'error':
_LOGGER.error("Wrong API key supplied. %s", tree[0].text)
else:
return NmaNotificationService(config[DOMAIN][CONF_API_KEY])
# pylint: disable=too-few-public-methods
class NmaNotificationService(BaseNotificationService):
""" Implements notification service for NMA. """
def __init__(self, api_key):
# pylint: disable=no-name-in-module, unused-variable
from requests import Session
self._api_key = api_key
self._data = {"apikey": self._api_key}
self.nma = Session()
def send_message(self, message="", **kwargs):
""" Send a message to a user. """
title = kwargs.get(ATTR_TITLE)
self._data['application'] = 'home-assistant'
self._data['event'] = title
self._data['description'] = message
self._data['priority'] = 0
response = self.nma.get(_RESOURCE + 'notify',
params=self._data)
tree = ET.fromstring(response.content)
if tree[0].tag == 'error':
_LOGGER.exception(
"Unable to perform request. Error: %s", tree[0].text)

View File

@@ -0,0 +1,76 @@
"""
homeassistant.components.notify.pushbullet
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
PushBullet platform for notify component.
Configuration:
To use the PushBullet notifier you will need to add something like the
following to your config/configuration.yaml
notify:
platform: pushbullet
api_key: YOUR_API_KEY
Variables:
api_key
*Required
Enter the API key for PushBullet. Go to https://www.pushbullet.com/ to retrieve
your API key.
"""
import logging
from homeassistant.helpers import validate_config
from homeassistant.components.notify import (
DOMAIN, ATTR_TITLE, BaseNotificationService)
from homeassistant.const import CONF_API_KEY
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pushbullet.py==0.7.1']
def get_service(hass, config):
""" Get the pushbullet notification service. """
if not validate_config(config,
{DOMAIN: [CONF_API_KEY]},
_LOGGER):
return None
try:
# pylint: disable=unused-variable
from pushbullet import PushBullet, InvalidKeyError # noqa
except ImportError:
_LOGGER.exception(
"Unable to import pushbullet. "
"Did you maybe not install the 'pushbullet.py' package?")
return None
try:
return PushBulletNotificationService(config[DOMAIN][CONF_API_KEY])
except InvalidKeyError:
_LOGGER.error(
"Wrong API key supplied. "
"Get it at https://www.pushbullet.com/account")
# pylint: disable=too-few-public-methods
class PushBulletNotificationService(BaseNotificationService):
""" Implements notification service for Pushbullet. """
def __init__(self, api_key):
from pushbullet import PushBullet
self.pushbullet = PushBullet(api_key)
def send_message(self, message="", **kwargs):
""" Send a message to a user. """
title = kwargs.get(ATTR_TITLE)
self.pushbullet.push_note(title, message)

View File

@@ -0,0 +1,102 @@
"""
homeassistant.components.notify.pushover
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Pushover platform for notify component.
Configuration:
To use the Pushover notifier you will need to add something like the following
to your config/configuration.yaml
notify:
platform: pushover
api_key: ABCDEFGHJKLMNOPQRSTUVXYZ
user_key: ABCDEFGHJKLMNOPQRSTUVXYZ
Variables:
api_key
*Required
This parameter is optional but should be configured, in order to get an API
key you should go to https://pushover.net and register a new application.
This is a quote from the pushover website regarding free/open source apps:
"If you are creating a client-side library, application, or open source project
that will be redistributed and installed by end-users, you may want to require
each of your users to register their own application rather than including your
own API token with the software."
When setting up the application I recommend using the icon located here:
https://home-assistant.io/images/favicon-192x192.png
user_key
*Required
To retrieve this value log into your account at https://pushover.net
"""
import logging
from homeassistant.helpers import validate_config
from homeassistant.components.notify import (
DOMAIN, ATTR_TITLE, BaseNotificationService)
from homeassistant.const import CONF_API_KEY
REQUIREMENTS = ['python-pushover==0.2']
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-variable
def get_service(hass, config):
""" Get the pushover notification service. """
if not validate_config(config,
{DOMAIN: ['user_key', CONF_API_KEY]},
_LOGGER):
return None
try:
# pylint: disable=no-name-in-module, unused-variable
from pushover import InitError
except ImportError:
_LOGGER.exception(
"Unable to import pushover. "
"Did you maybe not install the 'python-pushover.py' package?")
return None
try:
api_token = config[DOMAIN].get(CONF_API_KEY)
return PushoverNotificationService(
config[DOMAIN]['user_key'],
api_token)
except InitError:
_LOGGER.error(
"Wrong API key supplied. "
"Get it at https://pushover.net")
# pylint: disable=too-few-public-methods
class PushoverNotificationService(BaseNotificationService):
""" Implements notification service for Pushover. """
def __init__(self, user_key, api_token):
# pylint: disable=no-name-in-module, unused-variable
from pushover import Client
self._user_key = user_key
self._api_token = api_token
self.pushover = Client(
self._user_key, api_token=self._api_token)
def send_message(self, message="", **kwargs):
""" Send a message to a user. """
# pylint: disable=no-name-in-module
from pushover import RequestError
title = kwargs.get(ATTR_TITLE)
try:
self.pushover.send_message(message, title=title)
except RequestError:
_LOGGER.exception("Could not send pushover notification")

View File

@@ -0,0 +1,92 @@
"""
homeassistant.components.notify.slack
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Slack platform for notify component.
Configuration:
To use the Slack notifier you will need to add something like the following
to your config/configuration.yaml
notify:
platform: slack
api_key: ABCDEFGHJKLMNOPQRSTUVXYZ
default_channel: '#general'
Variables:
api_key
*Required
The slack API token to use for sending slack messages.
You can get your slack API token here https://api.slack.com/web?sudo=1
default_channel
*Required
The default channel to post to if no channel is explicitly specified when
sending the notification message.
"""
import logging
from homeassistant.helpers import validate_config
from homeassistant.components.notify import (
DOMAIN, BaseNotificationService)
from homeassistant.const import CONF_API_KEY
REQUIREMENTS = ['slacker==0.6.8']
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-variable
def get_service(hass, config):
""" Get the slack notification service. """
if not validate_config(config,
{DOMAIN: ['default_channel', CONF_API_KEY]},
_LOGGER):
return None
try:
# pylint: disable=no-name-in-module, unused-variable
from slacker import Error as SlackError
except ImportError:
_LOGGER.exception(
"Unable to import slacker. "
"Did you maybe not install the 'slacker.py' package?")
return None
try:
api_token = config[DOMAIN].get(CONF_API_KEY)
return SlackNotificationService(
config[DOMAIN]['default_channel'],
api_token)
except SlackError as ex:
_LOGGER.error(
"Slack authentication failed")
_LOGGER.exception(ex)
# pylint: disable=too-few-public-methods
class SlackNotificationService(BaseNotificationService):
""" Implements notification service for Slack. """
def __init__(self, default_channel, api_token):
from slacker import Slacker
self._default_channel = default_channel
self._api_token = api_token
self.slack = Slacker(self._api_token)
self.slack.auth.test()
def send_message(self, message="", **kwargs):
""" Send a message to a user. """
from slacker import Error as SlackError
channel = kwargs.get('channel', self._default_channel)
try:
self.slack.chat.post_message(channel, message)
except SlackError as ex:
_LOGGER.exception("Could not send slack notification")
_LOGGER.exception(ex)

Some files were not shown because too many files have changed in this diff Show More