Compare commits

...

244 Commits
0.19.4 ... 0.21

Author SHA1 Message Date
Paulus Schoutsen
d7b0929a32 Merge pull request #2183 from home-assistant/dev
0.21
2016-06-07 19:27:55 -07:00
Paulus Schoutsen
31489a56db Merge remote-tracking branch 'origin/master' into dev
Conflicts:
	homeassistant/const.py
2016-06-07 19:27:23 -07:00
Paulus Schoutsen
3e09a7360e Version bump to 0.21 2016-06-07 19:26:43 -07:00
Daniel Høyer Iversen
0cdd752d6c Fixed bug in google time travel (#2202)
Fixed bug in google time travel  when arrival time is given
2016-06-07 19:19:47 -07:00
Adam Mills
027c0b3168 Add turn_off_action to kodi media player (#2224)
A new configuration option `turn_off_action` is added to kodi. It may be
one of: none, quit, hibernate, suspend, reboot, or poweroff. The
appropriate command is sent to kodi when the turn_off action is
requested. Default value is none.

Kodi will only report turn_off supported if it is configured to
something other than none.
2016-06-07 19:18:25 -07:00
Paulus Schoutsen
271546d101 Merge branch 'pr/2251' into dev
Conflicts:
	homeassistant/components/switch/template.py
2016-06-07 19:16:14 -07:00
Johann Kellerman
d1ed17e7db Default parameter for .run() 2016-06-07 23:00:09 +02:00
Alex Harvey
fb2fb5ea73 zwave auto heal at midnight (#2213)
* zwave auto heal at midnight

* fix debug to info, running heal, any heal will send a logger event
2016-06-07 09:29:15 -07:00
John Arild Berentsen
202a8dba8e Hvac fix (#2221)
* Zwave hvac fix

* Zwave hvac fix and move max min temp to base

* Tests
2016-06-07 08:43:46 -07:00
Kyle Hendricks
042a482ef1 Add sensor for DTE Energy Bridge (#2247)
Currently only measures instantaneous energy usage in kW
2016-06-07 08:42:34 -07:00
Alexander Fortin
fff413e04e Improve vagrant provisioner resiliency (#2252)
This should make it easier to fix race conditions that might arise if
box is destroyed but setup_done placeholder file is not removed
properly
2016-06-07 08:35:40 -07:00
Dan Smith
e29459a1ae Merge pull request #2241 from kk7ds/unifi-3.2
Add support for UniFi Video >= 3.2.0
2016-06-06 20:33:45 -07:00
Dan Smith
49de55e75b Add support for UniFi Video >= 3.2.0
Unfortunately, Ubiquiti changed their (supposedly versioned) API in
3.2.0 which causes us to have to refer to cameras by id instead of
UUID. The firmware for 3.2.x also changed the on-camera login procedures
and snapshot functionality significantly.

This bumps the requirement for uvcclient to 0.9.0, which supports the
newer API and makes the tweaks necessary to interact properly.
2016-06-06 20:28:52 -07:00
Hugo D
ee4b1e2b78 The metric unit of pressure is mbar not mBar (#2248)
This is useful to vaoid having several graph for the same type of data
According to wikipedia:
Units derived from the bar include the megabar (symbol: Mbar),
kilobar (symbol: kbar), decibar (symbol: dbar), centibar (symbol: cbar),
and millibar (symbol: mbar or mb).
2016-06-06 08:00:26 -07:00
Johann Kellerman
ed44d28fc0 service helper replaced with script helper (#2242) 2016-06-06 07:36:04 -07:00
Johann Kellerman
d5f9c1bc01 Updated template switch to cache Script objects 2016-06-06 06:41:29 +02:00
Fabian Affolter
f69c900977 Add schema (#2226) 2016-06-05 16:00:51 -07:00
Fabian Affolter
9a7ea72fa0 Upgrade schiene to 0.17 (#2231) 2016-06-05 15:59:54 -07:00
Fabian Affolter
fd4a9cf7c5 Upgrade fuzzywuzzy to 0.10.0 (#2234) 2016-06-05 15:58:54 -07:00
Fabian Affolter
0fe375049a Upgrade slacker to 0.9.16 (#2235) 2016-06-05 15:58:21 -07:00
Fabian Affolter
69f2f0f34a Upgrade pysnmp to 4.3.2 (#2236)
* Upgrade pysnmp to 4.3.2

* Fix pylint issue
2016-06-05 15:57:46 -07:00
Alex Harvey
076fdc3f8b Add a robots.txt (#2207) 2016-06-05 18:48:59 -04:00
Johann Kellerman
8887c2a8af service helper replaced with script helper 2016-06-05 21:44:57 +02:00
Fabian Affolter
f4594027fd Upgrade blockchain to 1.3.3 (#2220) 2016-06-04 12:55:46 +02:00
Robbie Trencheny
59a0005e5c Add CORS to WSGI (#2209)
* Add CORS support to WSGI

* Remove X-HA-Access as a CORS header, because as @JshWright so elegantly put it: "CORS controls access to response headers, not request headers"
2016-06-03 12:53:43 -07:00
Johann Kellerman
9157f722a4 Update Qwikswitch library version (#2214) 2016-06-02 18:47:29 -07:00
Fabian Affolter
7f2a1c61da Upgrade python-telegram-bot to 4.2.0 (#2204) 2016-06-02 04:38:39 -07:00
Sam Riley
0eb9516ea7 Support for RFY protocol (#2199) 2016-06-02 03:48:42 -07:00
Greg Dowling
3bb3a70347 Merge pull request #2203 from home-assistant/bump_loopenergy
Bump pyloopenergy version.
2016-06-02 11:21:48 +01:00
pavoni
0262269b00 Bump loopenergy version. Increased interval before deciding connection is dead and reconnecting. 2016-06-02 11:08:24 +01:00
Greg Dowling
780d62ac5c Merge pull request #2201 from home-assistant/fix_pywemo_ssdp_decode_utf8
Bump pywemo version to fix ssdp discovery encoding issue.
2016-06-02 10:09:13 +01:00
pavoni
5fca9e170e Bump pywemo version to fix ssdp discovery encoding issue. 2016-06-02 09:58:54 +01:00
Jacob Tomlinson
ca7415e935 Added rfxtrx rollershutter (#2030)
* Added rfxtrx rollershutter

* Updated mock command with real one

* Corrected test string
2016-06-02 00:39:58 -07:00
Alex Harvey
26d3c3b0d6 Update PULL_REQUEST_TEMPLATE.md (#2198)
* Update PULL_REQUEST_TEMPLATE.md

* Update PULL_REQUEST_TEMPLATE.md
2016-06-01 23:57:03 -07:00
Paulus Schoutsen
81f8764bb8 Update frontend 2016-06-01 23:47:31 -07:00
Nolan Gilley
24d2eaa6ca flux platform as a switch (#2097)
* flux platform as a switch

* use track_time_change. broken :(

* use track_utc_time_change instead of track_time_change

* add some basic tests

* use brightness from RGB_to_xy

* config_schema validation

* back to platform schema. what was i doing?

* more broken tests :(

* 644

* fix some time bugs

* add working tests. config validation still not right

* bug fixes and more test cases.
2016-06-01 23:38:19 -07:00
Paulus Schoutsen
f868df1035 Fix Norway (#2197) 2016-06-01 23:02:46 -07:00
Paulus Schoutsen
d0988422d4 Merge pull request #2196 from home-assistant/revert-2192-expose-required-ssl-in-discoveries
Revert "Report whether SSL is required in discovery"
2016-06-01 22:48:17 -07:00
Robbie Trencheny
f522d95328 Revert "Report whether SSL is required in discovery" 2016-06-01 22:37:16 -07:00
Robbie Trencheny
c856c67790 Report whether SSL is required in discoverables, like /api/discovery_info and ZeroConf (#2192) 2016-06-01 19:45:19 -07:00
Paulus Schoutsen
6c5efd5b7e Merge pull request #2195 from home-assistant/hotfix-20-3
Hotfix 20 3
2016-06-01 17:39:42 -07:00
Paulus Schoutsen
c3b6086d80 Version bump to 0.20.3 2016-06-01 17:36:04 -07:00
Paulus Schoutsen
0d93369154 Optimize foreacast.io API calls 2016-06-01 17:35:50 -07:00
Paulus Schoutsen
4e064f91fd Merge pull request #2191 from home-assistant/forecast-api-calls
Optimize foreacast.io API calls
2016-06-01 17:32:55 -07:00
Fabian Affolter
f9e53ca22f Add windows 10 tile (#2166) 2016-06-01 14:04:08 -07:00
Paulus Schoutsen
f8bdc835f8 Optimize foreacast.io API calls 2016-06-01 09:20:29 -07:00
Fabian Affolter
1f602be80a Remove print (already covered by logger) (#2184) 2016-05-31 14:02:31 -07:00
Josh Wright
fe4d971427 Re-add config validation for the http component (#2186)
This commit adds back the config validation for the http component. It
was removed during the WSGI shuffle. This is just a direct copy of what
@robbiet480 added in ab294d12f7 (with some testing to verify it still
works).
2016-05-31 14:00:12 -07:00
Paul Philippov
e5efc2e430 Improve Internet Time calculation. (#2185) 2016-05-31 07:19:00 -07:00
Paulus Schoutsen
537a2a6ef6 Improve index.html template 2016-05-30 23:45:02 -07:00
Paulus Schoutsen
11cc065845 Merge remote-tracking branch 'origin/master' into dev
Conflicts:
	homeassistant/const.py
2016-05-30 10:40:50 -07:00
Paulus Schoutsen
3ac31b2c1b Fix broken tests + linting 2016-05-30 10:19:12 -07:00
Paulus Schoutsen
a91f937245 Fix linting issues 2016-05-30 10:08:49 -07:00
Fabian Affolter
fed2584d8a Add azimuth (#1951)
* Add azimuth

* Place elevation and azimuth together in update part
2016-05-29 15:03:29 -07:00
Paulus Schoutsen
eaa8e5f29d Merge branch 'pr/2139' into dev
Conflicts:
	.coveragerc
2016-05-29 15:02:24 -07:00
Bart274
65fbba0e79 List entity_ids in config and only react to them (#2144)
* List entity_ids in config and only react to them

This allows us to define a list of entity_ids in the config to make the
template sensor, binary sensor and switch only react to state changes of
these entities instead of listening to all state changes.

* Forgot to import the track_state_change function

* Changed test for added entity_ids to config

* Use default MATCH_ALL and remove event_listener
2016-05-29 14:34:21 -07:00
Alexander Fortin
19522b1f39 Feedreader: add file data storage (#2147)
Right now we ignore already parsed entries and store the information
at runtime, but it will not survive a restart. This patch adds storage
functionality storing pickled file into default config folder when
feed has `published_parsed` support.
2016-05-29 14:33:53 -07:00
Jan Harkes
afe84c2a8b Allow time condition windows to cross midnight. (#2158)
* Allow time condition windows to cross midnight.

* Address comments.

Fold _in_time_window back into the time() condition test.
Use specific time values to test the time window.
2016-05-29 14:32:32 -07:00
Warren Konkel
952436aa0b Insteon support for brightness (#2169)
* Insteon support for brightness

* Farcy fix for unused constants.

* Remove unused constant and fix whitespace.

* Prevent toggle switches from jumping between states.

* 255 not 256
2016-05-29 14:31:14 -07:00
Olimpiu Rob
8a577c8e0d Added Osram lightify platform (#2170)
* Added Osram Lightify light component

* Added color temperature and fade transition support to Osram Lightify

* Added Osram Lightify light component

* Added color temperature and fade transition support to Osram Lightify

* Updated docstring

* Added osramlightify to ignore list on coveragerc and updated docstrings

* Fixed linting issues
2016-05-29 14:29:49 -07:00
rubund
bf940bd1f3 Initial support for EnOcean (#2177)
* Initial support for EnOcean

Tested to work with:
 - Eltako FUD61 dimmer
 - Eltako FT55 battery-less switch
 - Permundo PSC234 (switch and power monitor)

* Rerun gen_requirements_all.py
2016-05-29 14:28:03 -07:00
rubund
03e8627b12 New option for the netatmo platform: station (#2178)
This is necessary if multiple weather stations are associated with
one Netatmo account.
2016-05-29 14:25:11 -07:00
Brent
e886303f08 Fixed roku exception when device is powered off or looses connection (#2173) 2016-05-29 14:24:06 -07:00
Fabian Affolter
4b0df51b40 Vendorize vincenty requirement (#2176) 2016-05-29 11:55:16 -07:00
Paulus Schoutsen
8494ac7cef Update frontend 2016-05-29 09:50:30 -07:00
Alexander Fortin
5076ebe43c Add Vagrant setup (#2171) 2016-05-28 23:58:09 -07:00
Paulus Schoutsen
05b2559df8 Update frontend 2016-05-28 23:28:57 -07:00
Paulus Schoutsen
70b74da3eb Update frontend 2016-05-28 18:38:46 -07:00
Paulus Schoutsen
9e0b107991 Update frontend 2016-05-28 11:32:35 -07:00
Paulus Schoutsen
92d05ccb5c Fix lint errors 2016-05-28 10:52:44 -07:00
Paulus Schoutsen
bfdb51a558 Bugfixes for urls with dates 2016-05-28 10:37:22 -07:00
Paulus Schoutsen
e10b00f341 Update frontend 2016-05-27 21:45:38 -07:00
Paulus Schoutsen
cd87c40bbf Update frontend 2016-05-27 01:29:48 -07:00
Paulus Schoutsen
d02bc3deaa Update frontend gzip 2016-05-26 23:08:15 -07:00
Paulus Schoutsen
1798df7686 Handle invalid dev ids for dev tracker + owntracks (#2174) 2016-05-26 21:49:44 -07:00
ntouran
d505398917 Locked in required version of python-lirc for LIRC component 2016-05-26 07:53:17 -07:00
s1gnalrunner
70d6ce5b79 Fixed issue with edimax SP-1101 switches (#2105)
* Fixed issue with edimax SP-1101 switches

* Added missing ValueError exception
2016-05-26 05:53:10 -07:00
Daniel Høyer Iversen
71452c11c1 Fix bug in google travel time. Default option dictionary must contain mode. (#2134) 2016-05-26 05:52:17 -07:00
ntouran
e30f2bf912 Cleanups to LIRC module 2016-05-25 22:26:00 -07:00
ntouran
262d95b7b1 Merge remote-tracking branch 'origin/dev' into lirc 2016-05-25 09:21:58 -07:00
William Scanlon
ca3da0e53e Round temp and percentage for octoprint sensors (#2128) 2016-05-25 09:10:59 -07:00
Fabian Affolter
49882255c4 Upgrade astral to 1.1 (#2131) 2016-05-25 09:10:08 -07:00
Scott Bartuska
3db31cb951 Update PyISY to 1.0.6 (#2133)
* Update PyISY to 1.0.6 

1.0.6 is the newest version of PyISY

* PyISY to 1.0.6
2016-05-25 09:09:40 -07:00
Paulus Schoutsen
415cfc2537 WSGI: Hide password in logs (#2164)
* WSGI: Hide password in logs

* Add auth + pw in logs tests
2016-05-24 23:19:37 -07:00
wokar
88bb136813 lg_netcast: fix exception on missing access_token (#2150)
* specified default value for acccess_token to prevent exception on init
2016-05-24 08:36:40 -07:00
Paulus Schoutsen
4cecc626f4 manifest.json: remove trailing commas 2016-05-23 22:45:35 -07:00
Paulus Schoutsen
644d5de890 Merge pull request #2154 from home-assistant/hotfix-20-2
Hotfix 20 2
2016-05-23 22:25:59 -07:00
ntouran
148b8c5055 Updated requirements for LIRC 2016-05-23 21:47:46 -07:00
ntouran
09161ae615 Moved lirc out of sensor package. 2016-05-23 21:36:48 -07:00
ntouran
c1f96aabb0 Changed LIRC component so that it just fires events on the bus. 2016-05-23 21:26:49 -07:00
Robbie Trencheny
343625d539 If we have duration_in_traffic use that as the state, otherwise use duration 2016-05-23 23:43:22 -04:00
Robbie Trencheny
2e10b4bf67 If no departure time is set, use now as the default. If departure time is set but does not have a :, assume its a preformed Unix timestamp and send along as raw input. Assume same for arrival_time. 2016-05-23 23:43:06 -04:00
Jan Harkes
dc8e55fb8b Don't even bother trying to kill stray child processes.
When we change our process group id we don't get keyboard interrupt
signals passed if our parent is a bash script.
2016-05-23 23:30:41 -04:00
Jan Harkes
d86a5a1e91 Don't even bother trying to kill stray child processes.
When we change our process group id we don't get keyboard interrupt
signals passed if our parent is a bash script.
2016-05-23 23:29:53 -04:00
Jan Harkes
1327051277 Version bump to 0.20.2 2016-05-23 23:29:53 -04:00
Josh Wright
712c51e283 Fix TLS with eventlet (#2151)
* Fix TLS with eventlet

This fixes a simple error on my part when implementing the WSGI stuff.

eventlet.wrap_ssl() returns a wrapped socket, it does not modify the
object passed to it. We need to grab the returned value and use that.

* Fix style issue
2016-05-23 17:39:55 -07:00
Robbie Trencheny
c96f73d1be If we have duration_in_traffic use that as the state, otherwise use duration 2016-05-23 14:05:12 -07:00
Robbie Trencheny
b3afb386b7 If no departure time is set, use now as the default. If departure time is set but does not have a :, assume its a preformed Unix timestamp and send along as raw input. Assume same for arrival_time. 2016-05-23 13:48:47 -07:00
Robbie Trencheny
2544635921 Update issue template to prettify the header. 2016-05-23 13:08:47 -07:00
ntouran
4e5b5f2204 LIRC: Responded to some code review requests but not the big one 2016-05-22 22:19:10 -07:00
Paulus Schoutsen
98de7c9287 Upgrade eventlet to 0.19 2016-05-22 20:14:46 -07:00
ntouran
80e60efd8f Removed LIRC dependency from requirements due to "complex" compliation
User will have to install lirc and python-lirc manually.
2016-05-22 16:28:20 -07:00
ntouran
b3e9e1dfcd added LIRC component to .coveragerc 2016-05-22 16:11:26 -07:00
ntouran
40bc49aaae Added LIRC component for responding to IR remote commands 2016-05-22 14:15:09 -07:00
Paulus Schoutsen
3c364fa7e9 Merge pull request #2132 from home-assistant/hotfix-20-1
Hotfix 0.20.1
2016-05-22 09:11:47 -07:00
Jan Harkes
05946ae5a2 Ignore assertions from python threading when looking for leaked threads. (#2130)
While looking for leaked resources (threads) after shutdown and before restart
we in some cases get an assertion in the python threading module where we find
a thread marked as running at the python level but it has no associated thread
at the C level.
2016-05-22 00:35:33 -04:00
Jan Harkes
ceb0ec5fa4 Ignore assertions from python threading when looking for leaked threads.
While looking for leaked resources (threads) after shutdown and before restart
we in some cases get an assertion in the python threading module where we find
a thread marked as running at the python level but it has no associated thread
at the C level.
2016-05-22 00:22:19 -04:00
Jan Harkes
a28196df9a Version bump to 0.20.1 2016-05-22 00:21:19 -04:00
Paulus Schoutsen
c7cc045acd Use only 1 event listener for event stream. 2016-05-21 18:24:03 -07:00
Paulus Schoutsen
225a672a92 Fix Dockerfile 2016-05-21 17:03:46 -07:00
Paulus Schoutsen
4d5eb0e3fc EventStream to sent ping on start to notify browser 2016-05-21 16:31:22 -07:00
Paulus Schoutsen
a68ab07e72 Another attempt to fix SSL in Docker 2016-05-21 16:23:03 -07:00
Paulus Schoutsen
ec4fe7e6e6 update frontend gz 2016-05-21 16:00:59 -07:00
Paulus Schoutsen
0b4b46d80b Merge pull request #2063 from home-assistant/feature/wsgi
Feature/wsgi
2016-05-21 15:14:12 -07:00
Paulus Schoutsen
3bbdd9fedd Remove unused import 2016-05-21 15:01:55 -07:00
Paulus Schoutsen
2ed135439a Remove gzip API 2016-05-21 15:01:35 -07:00
Paulus Schoutsen
1750b22e59 Gzip all the things 2016-05-21 15:01:35 -07:00
Paulus Schoutsen
9c5e7a9584 Add gzip for static resources 2016-05-21 15:01:35 -07:00
Paulus Schoutsen
9b03848a2e Comment out eventstream tests 2016-05-21 15:01:35 -07:00
Paulus Schoutsen
548d415f94 Clean up EventStream 2016-05-21 15:01:35 -07:00
Paulus Schoutsen
18be276b08 Make event stream tests work on Travis ? 2016-05-21 15:01:35 -07:00
Paulus Schoutsen
9aa9e57890 Cleanup 2016-05-21 15:01:35 -07:00
Paulus Schoutsen
8fe2654862 Update requirements with new static update 2016-05-21 15:01:34 -07:00
Paulus Schoutsen
794ff20987 Get EventStream working 2016-05-21 15:01:34 -07:00
Paulus Schoutsen
fe794d7fd8 Access camera images using access token 2016-05-21 15:01:34 -07:00
Paulus Schoutsen
585fbb1c02 Cache files in static folder for a year 2016-05-21 15:01:34 -07:00
Paulus Schoutsen
e4b697b1ed Generate gzip for frontend/mdi 2016-05-21 15:01:34 -07:00
Paulus Schoutsen
de5533e3c2 Fix auth frontend 2016-05-21 15:01:34 -07:00
Paulus Schoutsen
5aa0158761 Add url validators 2016-05-21 15:01:34 -07:00
Paulus Schoutsen
4d7555957c Fix camera 2016-05-21 15:01:34 -07:00
Josh Wright
aa34fe15b2 Friendlier exceptions for misconfigured views
If a view is missing a url or name attribute, this will result
in a more actionable exception being raised.
2016-05-21 15:01:34 -07:00
Paulus Schoutsen
1096232e17 More WIP 2016-05-21 15:01:34 -07:00
Josh Wright
54ecab7590 Improve view registration comments
Clarify that HomeAssistantWSGI.register_view() can handle either instantiated or uninstantiated view classes.
2016-05-21 15:01:33 -07:00
Paulus Schoutsen
15e329a588 Tons of fixes - WIP 2016-05-21 15:01:33 -07:00
Paulus Schoutsen
768c98d359 Fix import issues 2016-05-21 15:01:02 -07:00
Josh Wright
6490378de3 Add some missing view registrations 2016-05-21 15:01:01 -07:00
Josh Wright
d0320a9099 WIP: Add WSGI stack
This is a fair chunk of the way towards adding a WSGI compatible stack
for Home Assistant. The majot missing piece is auth/sessions. I was
undecided on implementing the current auth mechanism, or adding a new
mechanism (likely based on Werkzeug's signed cookies).

Plenty of TODOs...
2016-05-21 15:01:01 -07:00
Paulus Schoutsen
9116eb166b Version bump to 0.21.0.dev0 2016-05-21 14:19:06 -07:00
Paulus Schoutsen
37bd93a975 Version bump to 0.20 2016-05-21 14:17:02 -07:00
Paulus Schoutsen
b78765a41f Merge pull request #2113 from home-assistant/dev
0.20
2016-05-21 14:15:42 -07:00
Paulus Schoutsen
ab60b32326 Update frontend 2016-05-21 14:06:07 -07:00
Jan Harkes
3ea179cc0b Let systemd handle home-assistant process restarts. (#2127) 2016-05-21 12:58:14 -07:00
Paulus Schoutsen
5bedf5d604 Upgrade Nest to 2.9.2 (#2126) 2016-05-21 11:57:33 -07:00
Ardi Mehist
d8c1959715 Add support for Logentries (#1945)
* Add support for Logentries

Supports sending has events to Logentries web hook endpoint
see logentries.com for more

Inspired by the Splunk component

* bugfix

* fix summary

* fix test

* fix logentries url and tests

* update tests

* mock token

* Bug fixes

* typo

* typo

* fix string splitting

* remove redundant backslash
2016-05-21 11:21:23 -07:00
Robbie Trencheny
0f1c4d2f8c GTFS fixes (#2119)
* Change to official PyGTFS source

* Threading fixes for GTFS

* Actually pygtfs 0.1.3

* Update requirements_all.txt

* Update gtfs version
2016-05-21 11:04:18 -07:00
Igor Shults
3ce6c732ab #2120 Fix hvac z-wave fan list (#2121)
* #2120 Fix hvac z-wave fan list

* Properly name methods
2016-05-21 10:56:20 -07:00
Nolan Gilley
191fc8f8d4 Change color_RGB_to_xy formula & return brightness (#2095)
* Use RGB to XY calculations from Philips Hue developer site

* uppercase X,Y,Z

* rename cx,cy to x,y

* return brightness in color_RGB_to_xy

* remove try/catch

* update existing platforms using color_RGB_to_xy

* improve wemo w/ jaharkes suggestion

* allow brightness override of rgb_to_xy
2016-05-21 10:19:27 -07:00
Johann Kellerman
31c2d45a7a Updated pyqwikswitch & QS<->HA UI behaviour (#2123)
* Updated pyqwikswitch & constants

* Disable too-many-locals
2016-05-21 10:12:42 -07:00
Dan
dee6355cc5 Onkyo updates (#2084)
* use sane defaults for openzwave config

Use sane default if libopenzwave is installed. In most cases this will
mean that the zwave config path will not need to e manually specified.

* Resuming work on onkyo component

* Source control added to UI for onkyo receiver

Source will now display in the UI. Source mappings can be defined in the
config, and a rudimentary mapping is defined by default as a fallback.
When the onkyo source is updated, it will resolve to a defined name if
possible. This may break existing automations.

* fix lint errors

* Updated Onkyo receiver

Now takes an optional ip/name in additional to atempting to discover
deivces.

Source select will now take a sources mapping in the config. It will
provide default values if no source mapping is provided.

example:

- platform: onkyo
  host: 10.0.0.2
  name: receiver
  sources:
    HTPC: 'pc'
    Chromecast: 'aux1'
    Bluray: 'bd'
    Wii U: 'game'

* fix pylint error

* Use HA's error log instead of stack trace

* Flipped source mappings, code cleanup
2016-05-21 10:04:08 -07:00
Robbie Trencheny
c9b5ea97da Fix docstring issues with MoldIndicator 2016-05-21 10:03:24 -07:00
Felix
eaebe83429 Moldindicator Sensor (#1575)
* Adds MoldIndicator sensor platform

This sensor may be used to get an indication for possible mold growth in rooms.
It calculates the humidity at a pre-calibrated indoor point (wall, window).

* Automatic conversion to Fahrenheit for mold_indicator

* Minor change to critical temp label

* Fixed docstrings and styles

* Minor changes to MoldIndicator implementation

* Added first (non-working) implementation for mold_indicator test

* Small style changes

* Minor improvements to mold_indicator

* Completed unit test for mold indicator

* Fix to moldindicator initialization

* Adds missing period. Now that really matters..

* Adds test for sensor_changed function
2016-05-21 09:58:59 -07:00
Fabian Affolter
7f0b8c5e70 Docs (#2124)
* Add link to docs

* Update link
2016-05-21 16:59:52 +02:00
Jan Harkes
53d51a467d Single process restart fixes (#2118)
* Ignore permission errors on setpgid.

When launched in a docker container we got a permission denied error
from setpgid.

* Don't fail if we find our own pidfile.

When we restart using exec we are running a new instance of home-assistant with
the same process id so we shouldn't be surprised to find an existing pidfile in
that case.

* Allow restart to work when started as python -m homeassistant.

When we are started with `python -m homeassistant`, the restart command line
becomes `python /path/to/hass/homeassistant/__main__.py`. But in that case the
python path includes `/path/to/hass/homeassistant` instead of `/path/to/hass`
and we fail on the first import.

Fix this by recognizing `/__main__.py` as part of the first argument and
injecting the proper path as PYTHONPATH environment before we start the new
home-assistant instance.
2016-05-20 11:45:16 -07:00
Alexander Fortin
7eeb623b8f Add media_player.sonos_group_players service (#2087)
Sonos platform supports a `party mode` feature that groups all
available players into a single group, of which the calling player
will be the coordinator.
2016-05-20 09:54:15 -07:00
Jan Harkes
6b724f7da4 Not sure why, but this fixed a bad filedescriptor error. (#2116) 2016-05-20 07:03:08 -07:00
Alexander Fortin
a4409da700 Add add_uri_to_queue support to (sonos) media player (#1946)
Sonos (SoCo) supports add_uri_to_queue capability, making it possible
to stream media available via HTTP for example. This patch extends
media_player component and sonos platform to support this feature
2016-05-19 23:30:19 -07:00
John Arild Berentsen
1eb3181c14 Fix fitbit KeyError (#2077)
* Fix fitbit KeyError

* Set units compared to temperature_unit

* Pass true or false for is_metric
2016-05-19 23:28:53 -07:00
wokar
f7b401a20e Added the lg_netcast platform to control a LG Smart TV running NetCast 3.0 or 4.0 (#2081)
* Added the `lgtv` platform to control a LG Smart TV running NetCast 3.0
(LG Smart TV models released in 2012) and NetCast 4.0 (LG Smart TV models released in 2013).

* Fixed multi-line docstring closing quotes

* Rename lgtv to lg_netcast

* Rename lgtv to lg_netcast

* Extracted class to control the LG TV into a separate Python package 'pylgnetcast' and changed requirements accordingly.

* regenerated requirements_all.txt with script

* now uses pylgnetcast v0.2.0 which uses the requests package for the communication with the TV

* fixed lint error: Catching too general exception Exception
2016-05-19 23:27:47 -07:00
Jan Harkes
5f92ceeea9 Allow for restart without using parent/child processes. (#1793)
* Allow for restart without using parent/child processes.

Assuming that we normally correctly shut down running threads and
release resources, we just do some minimal scrubbing of open file
descriptors and child processes which would stay around across an
exec() boundary.

* Use sys.executable instead of multiprocessing.spawn.get_executable()

* Limit how many file descriptors we try to close.

Don't even try to close on OSX/Darwin until we figure out how to
recognize guarded fds because the kernel will yell at us, and kill
the process.

* Use the close on exec flag on MacOS to clean up.

* Introduce a small process runner to handle restart on windows.

* Handle missing signal.SIGHUP on Windows.
2016-05-19 23:20:59 -07:00
Per Sandström
f0f1fadee1 redirect daemon file descriptors (#2103) 2016-05-19 23:20:07 -07:00
Paulus Schoutsen
32f97dc578 Merge remote-tracking branch 'origin/master' into dev
Conflicts:
	homeassistant/const.py
2016-05-19 22:32:34 -07:00
Greg Dowling
631ba2ef0d Merge pull request #2110 from home-assistant/bump_loop_energy
Bump loop energy library version.
2016-05-19 17:19:06 +01:00
pavoni
62de16804b Bump loop energy library version. 2016-05-19 17:12:19 +01:00
Paulus Schoutsen
3d919f1235 Merge pull request #2108 from home-assistant/owntracks_fixes
Owntracks fixes
2016-05-19 08:28:36 -07:00
pavoni
8ff9506138 Ignore acc: 0 updates. 2016-05-19 16:16:43 +01:00
pavoni
dd1703469e Handle region enter/leave with spaces. 2016-05-19 16:04:55 +01:00
Daniel Høyer Iversen
5f98a70c21 Fix bug in flaky rfxtrx test (#2107) 2016-05-19 06:36:11 -07:00
Fabian Affolter
bfd64ce96e Upgrade python-telegram-bot to 4.1.1 (#2102) 2016-05-18 17:05:08 -07:00
Fabian Affolter
a032e649f5 Upgrade psutil to 4.2.0 (#2101) 2016-05-18 17:04:59 -07:00
Robbie Trencheny
c96a5d5b2b Fix profile usage with aws notify platforms (#2100) 2016-05-17 16:51:38 -07:00
Robbie Trencheny
a565cc4b73 Catch a gntp networkerror (#2099) 2016-05-17 16:51:32 -07:00
froz
8d34b76d51 Restored telnet as an option. Activate with config option 'protocol: telnet'. Default is ssh (#2096) 2016-05-17 15:55:12 -07:00
happyleavesaoc
15f89fc636 add some include_dir options (#2074)
* add some include_dir options

* validate, and extend instead of add

* add yaml include tests
2016-05-17 15:47:44 -07:00
Robbie Trencheny
a431277de1 Accept human readable color names to change light colors (#2075)
* Add support for providing color_name which accepts a CSS3 valid, human readable string such as red or blue

* Forgot the schema validation!

* ugh farcy

* use html5_parse_legacy_color for more input options

* Add webcolors==1.5 to setup.py

* Block pylint no-member errors on tuple

* add color_name_to_rgb test

* whoops

* revert changes to individual platforms

* If color_name is set, pop it off params and set rgb_color with it

* Forgot to reset wink.py

* Import the legacy function as color_name_to_rgb directly

* reset test_color.py

* Improve light services.yaml
2016-05-17 00:06:55 -07:00
Alexander Fortin
7208ff515e Better handle exceptions from Sonos players (#2085)
Sonos players can be dynamically set in various modes, for example
as TV players or Line-IN or straming from radios channels, therefore
some methods could not be available, and when invoked they cause
long exceptions to be logged. This partially solves the problem
reducing the output and logging some more informative error message
2016-05-16 22:58:57 -07:00
Paulus Schoutsen
0a79a5e964 Update frontend repo 2016-05-16 21:59:39 -07:00
Daniel Høyer Iversen
8e766daa11 Merge pull request #2086 from home-assistant/time_travel_fix
Round minutes to integer in google travel time, Fix issue #2080
2016-05-16 11:45:43 +02:00
Daniel
4ded795740 Round minutes to integer in google travel time, Fix issue #2080 2016-05-16 11:37:17 +02:00
Robbie Trencheny
84cb7a4f20 Add AWS notify platforms (Lambda, SNS, SQS) (#2073)
* AWS SNS notify platform

* Attach kwargs as MessageAttributes

* Initial pass of AWS SQS platform

* Add Lambda notify platform

* Remove unused import

* Change single quotes to double quotes because I am crazy

* Forgot to run pydocstyle

* Improve context support for Lambda

* compress the message_attributes logic
2016-05-15 13:17:35 -07:00
Rowan
cbf0caa88a Last.fm sensor (#2071)
* Last.fm component

* Pylint fixes

* Last.fm component

* Pylint fixes

* Updated with `.coveragerc` and `requirements_all.txt`

* Pylint fixes

* Updated

* Pylint fix

* Pylint fix
2016-05-15 13:11:41 -07:00
Brent
88d13f0ac9 Added support for the roku media player (#2046) 2016-05-15 13:00:31 -07:00
mnestor
3ed6be5b4e add link ability to configurator (#2035) 2016-05-15 12:56:29 -07:00
Richard Cox
0340710e5c Support for Nest Protect smoke alarms (#2076)
* Support for Nest Protect smoke alarms

* Fixing formatting issues from tox
2016-05-15 12:29:12 -07:00
froz
49acdaa8fd Device Tracker - ASUSWRT: Replaced telnet with ssh (#2079) 2016-05-15 12:20:17 -07:00
Alex Harvey
ffbc99fac2 Merge pull request #2059 from infamy/justyns-purge_old_data
Justyns purge old data
2016-05-14 23:55:28 -07:00
Johann Kellerman
6dae005b65 Resolved UI flicker, new config vars, brightness up to 255, fixed buttons, fixed race condition (#2072) 2016-05-14 14:21:05 -07:00
Robbie Trencheny
0adc853741 Add notify.twilio_sms component (#2070) 2016-05-14 14:09:28 -07:00
Robbie Trencheny
6254d4a983 Add lines for associated documentation PR 2016-05-14 14:02:14 -07:00
Robbie Trencheny
a7db208b8a Fix Google Voice documentation URL 2016-05-14 13:32:00 -07:00
Rowan
429bf2c143 Google Play Music Desktop Player component (#1788)
* Added GPM Desktop Plaeyr component

* Updated requirements_all.txt

* Pylint fix

* Updated GPMDP.py to include @balloob's comments

* Updated to work with the latest version of GPMDP

* Removed setting "self._ws.recv()" as a variable

* Made line 52 shorter

* Updated to check weather it is connected or not

* Pylint and @balloob fixes

* Updated with simplified code and pylint fix

* Made `json.loads` shorter

* Pylint fix
2016-05-14 13:28:42 -07:00
happyleavesaoc
8df91e6a17 numeric state: validate multiple entities (#2066)
* validate multiple entities

* point to current entity
2016-05-14 12:29:57 -07:00
Daniel Høyer Iversen
8656bbbc79 fix bugs in google travel time (#2069) 2016-05-14 12:14:13 -07:00
Daniel Høyer Iversen
630b7377bd Refactor get_age in util/dt (#2067) 2016-05-14 12:05:46 -07:00
Daniel Høyer Iversen
0626a80186 Merge pull request #2068 from home-assistant/yaml_env
Add test for yaml enviroment
2016-05-14 20:33:46 +02:00
Daniel
24788b106b Add test for yaml enviroment 2016-05-14 20:20:27 +02:00
Igor Shults
c5401b21c2 Fix typo in system monitor ('recieved') (#2062) 2016-05-14 09:45:32 -07:00
mnestor
954b56475e YAML: add !include_named_dir and ! include_list_dir (#2054)
* add include_dir constructor for yaml parsing

* changed to allow for flat and name based directory including

* fixed ci errors

* changed flat to list
2016-05-13 21:16:04 -07:00
Alex Harvey
53d7e0730c Fixes for farcy 2016-05-13 14:43:22 -07:00
Alex Harvey
cba85cad8d Fixes for farcy 2016-05-13 14:42:08 -07:00
Lewis Juggins
96b73684eb Update Dockerfile to use OpenSSL 1.0.2h to resolve certificate issues (#2057) 2016-05-13 07:55:52 -07:00
Robbie Trencheny
aa7fa7b550 Dont default to driving anymore, re: #2047 2016-05-12 22:49:12 -07:00
Robbie Trencheny
d229cb46b1 Google travel time improvements (#2047)
* Update google_travel_time.py

* Update google_travel_time.py

* pylint: disable=too-many-instance-attributes

* Add the mode to the title of the sensor

* Expose the travel mode on the sensor attributes

* Big improvements to the Google Travel Time sensor. Allow passing any options that Google supports in the options dict of your configuration. Deprecate travel_mode. Change name format to show the mode

* fu farcy

* Dynamically convert departure and arrival times

* Add a warning if user provides both departure and arrival times

* Add deprecation warning for travel_mode outside options and other minor fixes

* Use a copy of options dict to not overwrite the departure/arrival times constantly.

* Remove default travel_mode, but set default options.mode to driving

* Google doesnt let us query time in the past, so if the date we generate from a time string is in the past, add 1 day

* spacing fix

* Add config validation for all possible parameters

* flake8 and pylint fixes
2016-05-12 22:37:08 -07:00
happyleavesaoc
8682e2def8 supervisord sensor (#2056) 2016-05-12 22:16:58 -07:00
Johann Kellerman
65ac1ae84a Added QwikSwitch component & platforms (#1970)
* Added QwikSwitch platform

farcy - worst than my english teacher

* Clean up comments

* Import only inside functions

* Moved imports, no global var, load_platform

* add_device reworked

* Only serializable content on bus

* Fixed imports & removed some logging
2016-05-12 21:39:30 -07:00
Alex Harvey
93fd6fa11b fixes for pep and delay start 2016-05-12 10:33:22 -07:00
Alex Harvey
67b0365f62 update to latest base 2016-05-12 10:32:28 -07:00
Paulus Schoutsen
f1eda430cd Update rpi_rf.py 2016-05-12 00:13:48 -07:00
Paulus Schoutsen
69929f15fb Ignore RPI-RF in requirements_all 2016-05-11 22:56:05 -07:00
Robbie Trencheny
d553c7c8e7 Merge pull request #2027 from robbiet480/relative-time-filter
Add a Jinja filter for relative time
2016-05-11 22:50:56 -07:00
Robbie Trencheny
4d0b9f1e94 Stupid blank lines 2016-05-11 22:44:44 -07:00
Robbie Trencheny
fca4ec2b3e simplify the relative_time function 2016-05-11 22:37:37 -07:00
Robbie Trencheny
b75aa6ac08 Add get_age tests 2016-05-11 22:29:55 -07:00
Nolan Gilley
894ceacd40 Add Ecobee notify platform (#2021)
* add send_message to ecobee via service call

* farcy fixes

* fix pydocstyle

* ecobee notify component
2016-05-11 22:03:21 -07:00
Johann Kellerman
fbe940139a Discovery listener on all EntityComponents (#2042) 2016-05-11 21:58:22 -07:00
happyleavesaoc
c341ae0a39 Media Player - MPD: handle more exceptions (#2045) 2016-05-11 21:53:56 -07:00
Nolan Gilley
f9d97c4356 fix away mode. issue 2032 (#2044) 2016-05-11 21:52:56 -07:00
Nolan Gilley
b8a5d392c5 Fix speedtest by removing Throttle and adding second parameter for track_time_change (#2040) 2016-05-11 08:24:50 -07:00
Paulus Schoutsen
3c9e493494 Make AND and OR conditions valid (#2037) 2016-05-10 21:49:58 -07:00
Erik Eriksson
2f118c5327 log received mqtt messages (#2031) 2016-05-10 21:12:14 -07:00
Nolan Gilley
a7d1f52ac8 Use Throttle on speedtest update (#2036)
* use throttle

* fix flake8
2016-05-10 20:51:55 -07:00
Paulus Schoutsen
c7cfa8d245 Update README.rst 2016-05-10 13:36:03 -07:00
Robbie Trencheny
16933abce9 Remove humanize and use a relative time thing that @balloob found on Github 2016-05-10 00:04:53 -07:00
Landrash
8163b986c9 Fixed minor miss-spelling (#2028)
Changed millileters to milliliters.
Changed case of mmol/l to mmol/L.
2016-05-09 23:48:48 -07:00
Robbie Trencheny
d5a1c52359 Add a Jinja filter for relative time 2016-05-09 23:31:02 -07:00
Nolan Gilley
26ea4e41cb Bring back custom scan intervals and service for speedtest.net component (#1980)
* Bring back the functionality that was removed in PR 1717. This includes the speedtest service and the ability to define the scan times in the configuration file.  Restore default functionality of 1 scan per hour on the hour.

* remove unnecessary code.
2016-05-09 22:49:26 -07:00
Johann Kellerman
ec9544b9c3 Add a load_platform mechanism (#2012)
* discovery.load_platform method

* rm grep
2016-05-09 22:48:03 -07:00
Fabian Affolter
1d0bc1ee66 Upgrade flake8 to 2.5.4 (#2018) 2016-05-09 22:33:21 -07:00
Robbie Trencheny
9729c44d53 Merge pull request #2023 from philipbl/fix_slack
Fix problem with Slack default channel
2016-05-09 16:37:38 -07:00
Robbie Trencheny
a7292af3b1 Fitbit flake8 and pylint fixes. Forgot to do it before pushing :( 2016-05-09 15:33:04 -07:00
Robbie Trencheny
c8cbc528eb Minor Fitbit tweaks. Correct the copy, dont require auth on the routes, get the client_id/client_secret from fitbit.conf instead of the YAML 2016-05-09 15:31:47 -07:00
Philip Lundrigan
8735bfe926 Fix problem with default channel 2016-05-09 16:19:19 -06:00
Robbie Trencheny
25e8c7bc5f en_UK->en_GB. Closes #2019. 2016-05-09 15:14:33 -07:00
jazzaj
499257c8e1 Corrected link to documentation (#2022) 2016-05-09 23:30:22 +02:00
Paulus Schoutsen
6856283896 Make HVAC naming consistent (#2017) 2016-05-09 07:53:01 -07:00
Paulus Schoutsen
20dad9f194 Add HVAC to demo 2016-05-08 23:21:26 -07:00
Paulus Schoutsen
09483e3be4 More fault tolerant discovery 2016-05-08 21:23:03 -07:00
John Arild Berentsen
ab2e85840f Fix for not recognizing Z-Wave thermostats (#2006)
* Fix for not recognizing thermostats

* Properly ignore zxt-120

* fix
2016-05-08 09:52:16 -07:00
Paulus Schoutsen
8257e3f384 Fix automation deprecation warning 2016-05-07 22:31:22 -07:00
Paulus Schoutsen
e40908d67c Improve config validation error message 2016-05-07 22:31:22 -07:00
Paulus Schoutsen
e67729b2f4 Version bump to 0.20.0.dev0 2016-05-07 12:55:41 -07:00
Justyn Shull
bf3b77e1f2 Change sqlite queries to work with older versions of sqlite 2016-04-15 21:18:51 -05:00
Justyn Shull
d5ca97b1f6 Add tests for purging old states and events 2016-04-15 21:02:17 -05:00
Justyn Shull
fd48fc5f83 Add CONFIG_SCHEMA to verify config. Move purge_days key name to
CONF_PURGE_DAYS so it can be changed easier later.

Use 'recorder' domain instead of 'history' domain.

Pass purge_days config directly into Recorder object instead of passing
the config object around.
2016-04-15 19:54:30 -05:00
Justyn Shull
c89cd6a68c Add 'purge_days' option to the history/recorder component
Issue https://github.com/balloob/home-assistant/issues/1337

When purge_days is set under the history component, recorder.py will
delete all events and states that are older than purge_days days ago.

Currently, this is only done once at start up.   A vacuum command is
also run to free up the disk space sqlite would still use after deleting
records.
2016-04-15 19:54:30 -05:00
198 changed files with 7526 additions and 1905 deletions

View File

@@ -38,6 +38,9 @@ omit =
homeassistant/components/octoprint.py
homeassistant/components/*/octoprint.py
homeassistant/components/qwikswitch.py
homeassistant/components/*/qwikswitch.py
homeassistant/components/rpi_gpio.py
homeassistant/components/*/rpi_gpio.py
@@ -72,6 +75,9 @@ omit =
homeassistant/components/zwave.py
homeassistant/components/*/zwave.py
homeassistant/components/enocean.py
homeassistant/components/*/enocean.py
homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/binary_sensor/arest.py
@@ -108,21 +114,29 @@ omit =
homeassistant/components/light/hyperion.py
homeassistant/components/light/lifx.py
homeassistant/components/light/limitlessled.py
homeassistant/components/light/osramlightify.py
homeassistant/components/lirc.py
homeassistant/components/media_player/cast.py
homeassistant/components/media_player/denon.py
homeassistant/components/media_player/firetv.py
homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/lg_netcast.py
homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/onkyo.py
homeassistant/components/media_player/panasonic_viera.py
homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/plex.py
homeassistant/components/media_player/roku.py
homeassistant/components/media_player/samsungtv.py
homeassistant/components/media_player/snapcast.py
homeassistant/components/media_player/sonos.py
homeassistant/components/media_player/squeezebox.py
homeassistant/components/media_player/yamaha.py
homeassistant/components/notify/aws_lambda.py
homeassistant/components/notify/aws_sns.py
homeassistant/components/notify/aws_sqs.py
homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py
homeassistant/components/notify/googlevoice.py
@@ -138,6 +152,7 @@ omit =
homeassistant/components/notify/smtp.py
homeassistant/components/notify/syslog.py
homeassistant/components/notify/telegram.py
homeassistant/components/notify/twilio_sms.py
homeassistant/components/notify/twitter.py
homeassistant/components/notify/xmpp.py
homeassistant/components/scene/hunterdouglas_powerview.py
@@ -146,6 +161,7 @@ omit =
homeassistant/components/sensor/cpuspeed.py
homeassistant/components/sensor/deutsche_bahn.py
homeassistant/components/sensor/dht.py
homeassistant/components/sensor/dte_energy_bridge.py
homeassistant/components/sensor/efergy.py
homeassistant/components/sensor/eliqonline.py
homeassistant/components/sensor/fitbit.py
@@ -153,6 +169,7 @@ omit =
homeassistant/components/sensor/glances.py
homeassistant/components/sensor/google_travel_time.py
homeassistant/components/sensor/gtfs.py
homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/netatmo.py
homeassistant/components/sensor/neurio_energy.py
@@ -163,6 +180,7 @@ omit =
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/steam_online.py
homeassistant/components/sensor/supervisord.py
homeassistant/components/sensor/swiss_public_transport.py
homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/temper.py

View File

@@ -1,4 +1,6 @@
Make sure you run the latest version before reporting an issue. Feature requests should go in the forum: https://community.home-assistant.io/c/feature-requests
Make sure you are running the latest version of Home Assistant before reporting an issue.
You should only file an issue if you found a bug. Feature and enhancement requests should go in [the Feature Requests section](https://community.home-assistant.io/c/feature-requests) of our community forum:
**Home Assistant release (`hass --version`):**

View File

@@ -1,7 +1,9 @@
**Description:**
**Related issue (if applicable):** #
**Related issue (if applicable):** fixes #
**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io#
**Example entry for `configuration.yaml` (if applicable):**
```yaml
@@ -10,6 +12,9 @@
**Checklist:**
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
If code communicates with devices:
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).

7
.gitignore vendored
View File

@@ -83,3 +83,10 @@ venv
# vimmy stuff
*.swp
*.swo
ctags.tmp
# vagrant stuff
virtualization/vagrant/setup_done
virtualization/vagrant/.vagrant
virtualization/vagrant/config

View File

@@ -19,7 +19,8 @@ RUN script/build_python_openzwave && \
ln -sf /usr/src/app/build/python-openzwave/openzwave/config /usr/local/share/python-openzwave/config
COPY requirements_all.txt requirements_all.txt
RUN pip3 install --no-cache-dir -r requirements_all.txt
# certifi breaks Debian based installs
RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi
# Copy source
COPY . .

View File

@@ -3,11 +3,11 @@ from __future__ import print_function
import argparse
import os
import signal
import platform
import subprocess
import sys
import threading
import time
from multiprocessing import Process
from homeassistant.const import (
__version__,
@@ -87,8 +87,7 @@ def get_arguments():
parser.add_argument(
'--debug',
action='store_true',
help='Start Home Assistant in debug mode. Runs in single process to '
'enable use of interactive debuggers.')
help='Start Home Assistant in debug mode')
parser.add_argument(
'--open-ui',
action='store_true',
@@ -123,15 +122,20 @@ def get_arguments():
'--restart-osx',
action='store_true',
help='Restarts on OS X.')
if os.name != "nt":
parser.add_argument(
'--runner',
action='store_true',
help='On restart exit with code {}'.format(RESTART_EXIT_CODE))
if os.name == "posix":
parser.add_argument(
'--daemon',
action='store_true',
help='Run Home Assistant as daemon')
arguments = parser.parse_args()
if os.name == "nt":
if os.name != "posix" or arguments.debug or arguments.runner:
arguments.daemon = False
return arguments
@@ -144,13 +148,21 @@ def daemonize():
# Decouple fork
os.setsid()
os.umask(0)
# Create second fork
pid = os.fork()
if pid > 0:
sys.exit(0)
# redirect standard file descriptors to devnull
infd = open(os.devnull, 'r')
outfd = open(os.devnull, 'a+')
sys.stdout.flush()
sys.stderr.flush()
os.dup2(infd.fileno(), sys.stdin.fileno())
os.dup2(outfd.fileno(), sys.stdout.fileno())
os.dup2(outfd.fileno(), sys.stderr.fileno())
def check_pid(pid_file):
"""Check that HA is not already running."""
@@ -161,6 +173,10 @@ def check_pid(pid_file):
# PID File does not exist
return
# If we just restarted, we just found our own pidfile.
if pid == os.getpid():
return
try:
os.kill(pid, 0)
except OSError:
@@ -220,29 +236,61 @@ def uninstall_osx():
print("Home Assistant has been uninstalled.")
def setup_and_run_hass(config_dir, args, top_process=False):
"""Setup HASS and run.
def closefds_osx(min_fd, max_fd):
"""Make sure file descriptors get closed when we restart.
Block until stopped. Will assume it is running in a subprocess unless
top_process is set to true.
We cannot call close on guarded fds, and we cannot easily test which fds
are guarded. But we can set the close-on-exec flag on everything we want to
get rid of.
"""
from fcntl import fcntl, F_GETFD, F_SETFD, FD_CLOEXEC
for _fd in range(min_fd, max_fd):
try:
val = fcntl(_fd, F_GETFD)
if not val & FD_CLOEXEC:
fcntl(_fd, F_SETFD, val | FD_CLOEXEC)
except IOError:
pass
def cmdline():
"""Collect path and arguments to re-execute the current hass instance."""
if sys.argv[0].endswith('/__main__.py'):
modulepath = os.path.dirname(sys.argv[0])
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon']
def setup_and_run_hass(config_dir, args):
"""Setup HASS and run."""
from homeassistant import bootstrap
# Run a simple daemon runner process on Windows to handle restarts
if os.name == 'nt' and '--runner' not in sys.argv:
args = cmdline() + ['--runner']
while True:
try:
subprocess.check_call(args)
sys.exit(0)
except subprocess.CalledProcessError as exc:
if exc.returncode != RESTART_EXIT_CODE:
sys.exit(exc.returncode)
if args.demo_mode:
config = {
'frontend': {},
'demo': {}
}
hass = bootstrap.from_config_dict(
config, config_dir=config_dir, daemon=args.daemon,
verbose=args.verbose, skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days)
config, config_dir=config_dir, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days)
else:
config_file = ensure_config_file(config_dir)
print('Config directory:', config_dir)
hass = bootstrap.from_config_file(
config_file, daemon=args.daemon, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days)
config_file, verbose=args.verbose, skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days)
if hass is None:
return
@@ -259,39 +307,49 @@ def setup_and_run_hass(config_dir, args, top_process=False):
hass.start()
exit_code = int(hass.block_till_stopped())
if not top_process:
sys.exit(exit_code)
return exit_code
def run_hass_process(hass_proc):
"""Run a child hass process. Returns True if it should be restarted."""
requested_stop = threading.Event()
hass_proc.daemon = True
def request_stop(*args):
"""Request hass stop, *args is for signal handler callback."""
requested_stop.set()
hass_proc.terminate()
def try_to_restart():
"""Attempt to clean up state and start a new homeassistant instance."""
# Things should be mostly shut down already at this point, now just try
# to clean up things that may have been left behind.
sys.stderr.write('Home Assistant attempting to restart.\n')
# Count remaining threads, ideally there should only be one non-daemonized
# thread left (which is us). Nothing we really do with it, but it might be
# useful when debugging shutdown/restart issues.
try:
signal.signal(signal.SIGTERM, request_stop)
nthreads = sum(thread.isAlive() and not thread.isDaemon()
for thread in threading.enumerate())
if nthreads > 1:
sys.stderr.write(
"Found {} non-daemonic threads.\n".format(nthreads))
# Somehow we sometimes seem to trigger an assertion in the python threading
# module. It seems we find threads that have no associated OS level thread
# which are not marked as stopped at the python level.
except AssertionError:
sys.stderr.write("Failed to count non-daemonic threads.\n")
# Try to not leave behind open filedescriptors with the emphasis on try.
try:
max_fd = os.sysconf("SC_OPEN_MAX")
except ValueError:
print('Could not bind to SIGTERM. Are you running in a thread?')
max_fd = 256
hass_proc.start()
try:
hass_proc.join()
except KeyboardInterrupt:
request_stop()
try:
hass_proc.join()
except KeyboardInterrupt:
return False
if platform.system() == 'Darwin':
closefds_osx(3, max_fd)
else:
os.closerange(3, max_fd)
return (not requested_stop.isSet() and
hass_proc.exitcode == RESTART_EXIT_CODE,
hass_proc.exitcode)
# Now launch into a new instance of Home-Assistant. If this fails we
# fall through and exit with error 100 (RESTART_EXIT_CODE) in which case
# systemd will restart us when RestartForceExitStatus=100 is set in the
# systemd.service file.
sys.stderr.write("Restarting Home-Assistant\n")
args = cmdline()
os.execv(args[0], args)
def main():
@@ -325,21 +383,10 @@ def main():
if args.pid_file:
write_pid(args.pid_file)
# Run hass in debug mode if requested
if args.debug:
sys.stderr.write('Running in debug mode. '
'Home Assistant will not be able to restart.\n')
exit_code = setup_and_run_hass(config_dir, args, top_process=True)
if exit_code == RESTART_EXIT_CODE:
sys.stderr.write('Home Assistant requested a '
'restart in debug mode.\n')
return exit_code
exit_code = setup_and_run_hass(config_dir, args)
if exit_code == RESTART_EXIT_CODE and not args.runner:
try_to_restart()
# Run hass as child process. Restart if necessary.
keep_running = True
while keep_running:
hass_proc = Process(target=setup_and_run_hass, args=(config_dir, args))
keep_running, exit_code = run_hass_process(hass_proc)
return exit_code

View File

@@ -215,7 +215,7 @@ def mount_local_lib_path(config_dir):
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments
def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
verbose=False, daemon=False, skip_pip=False,
verbose=False, skip_pip=False,
log_rotate_days=None):
"""Try to configure Home Assistant from a config dict.
@@ -240,7 +240,7 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
process_ha_config_upgrade(hass)
if enable_log:
enable_logging(hass, verbose, daemon, log_rotate_days)
enable_logging(hass, verbose, log_rotate_days)
hass.config.skip_pip = skip_pip
if skip_pip:
@@ -278,8 +278,8 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
return hass
def from_config_file(config_path, hass=None, verbose=False, daemon=False,
skip_pip=True, log_rotate_days=None):
def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
log_rotate_days=None):
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter if given,
@@ -293,7 +293,7 @@ def from_config_file(config_path, hass=None, verbose=False, daemon=False,
hass.config.config_dir = config_dir
mount_local_lib_path(config_dir)
enable_logging(hass, verbose, daemon, log_rotate_days)
enable_logging(hass, verbose, log_rotate_days)
try:
config_dict = config_util.load_yaml_config_file(config_path)
@@ -304,28 +304,27 @@ def from_config_file(config_path, hass=None, verbose=False, daemon=False,
skip_pip=skip_pip)
def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
def enable_logging(hass, verbose=False, log_rotate_days=None):
"""Setup the logging."""
if not daemon:
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:
pass
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:
pass
# Log errors to a file if we have write access to file or config dir
err_log_path = hass.config.path(ERROR_LOG_FILENAME)

View File

@@ -7,14 +7,14 @@ https://home-assistant.io/components/alexa/
import enum
import logging
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import template, script
from homeassistant.components.http import HomeAssistantView
DOMAIN = 'alexa'
DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__)
_CONFIG = {}
API_ENDPOINT = '/api/alexa'
@@ -26,80 +26,88 @@ CONF_ACTION = 'action'
def setup(hass, config):
"""Activate Alexa component."""
intents = config[DOMAIN].get(CONF_INTENTS, {})
for name, intent in intents.items():
if CONF_ACTION in intent:
intent[CONF_ACTION] = script.Script(hass, intent[CONF_ACTION],
"Alexa intent {}".format(name))
_CONFIG.update(intents)
hass.http.register_path('POST', API_ENDPOINT, _handle_alexa, True)
hass.wsgi.register_view(AlexaView(hass,
config[DOMAIN].get(CONF_INTENTS, {})))
return True
def _handle_alexa(handler, path_match, data):
"""Handle Alexa."""
_LOGGER.debug('Received Alexa request: %s', data)
class AlexaView(HomeAssistantView):
"""Handle Alexa requests."""
req = data.get('request')
url = API_ENDPOINT
name = 'api:alexa'
if req is None:
_LOGGER.error('Received invalid data from Alexa: %s', data)
handler.write_json_message(
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
return
def __init__(self, hass, intents):
"""Initialize Alexa view."""
super().__init__(hass)
req_type = req['type']
for name, intent in intents.items():
if CONF_ACTION in intent:
intent[CONF_ACTION] = script.Script(
hass, intent[CONF_ACTION], "Alexa intent {}".format(name))
if req_type == 'SessionEndedRequest':
handler.send_response(HTTP_OK)
handler.end_headers()
return
self.intents = intents
intent = req.get('intent')
response = AlexaResponse(handler.server.hass, intent)
def post(self, request):
"""Handle Alexa."""
data = request.json
if req_type == 'LaunchRequest':
response.add_speech(
SpeechType.plaintext,
"Hello, and welcome to the future. How may I help?")
handler.write_json(response.as_dict())
return
_LOGGER.debug('Received Alexa request: %s', data)
if req_type != 'IntentRequest':
_LOGGER.warning('Received unsupported request: %s', req_type)
return
req = data.get('request')
intent_name = intent['name']
config = _CONFIG.get(intent_name)
if req is None:
_LOGGER.error('Received invalid data from Alexa: %s', data)
return self.json_message('Expected request value not received',
HTTP_BAD_REQUEST)
if config is None:
_LOGGER.warning('Received unknown intent %s', intent_name)
response.add_speech(
SpeechType.plaintext,
"This intent is not yet configured within Home Assistant.")
handler.write_json(response.as_dict())
return
req_type = req['type']
speech = config.get(CONF_SPEECH)
card = config.get(CONF_CARD)
action = config.get(CONF_ACTION)
if req_type == 'SessionEndedRequest':
return None
# pylint: disable=unsubscriptable-object
if speech is not None:
response.add_speech(SpeechType[speech['type']], speech['text'])
intent = req.get('intent')
response = AlexaResponse(self.hass, intent)
if card is not None:
response.add_card(CardType[card['type']], card['title'],
card['content'])
if req_type == 'LaunchRequest':
response.add_speech(
SpeechType.plaintext,
"Hello, and welcome to the future. How may I help?")
return self.json(response)
if action is not None:
action.run(response.variables)
if req_type != 'IntentRequest':
_LOGGER.warning('Received unsupported request: %s', req_type)
return self.json_message(
'Received unsupported request: {}'.format(req_type),
HTTP_BAD_REQUEST)
handler.write_json(response.as_dict())
intent_name = intent['name']
config = self.intents.get(intent_name)
if config is None:
_LOGGER.warning('Received unknown intent %s', intent_name)
response.add_speech(
SpeechType.plaintext,
"This intent is not yet configured within Home Assistant.")
return self.json(response)
speech = config.get(CONF_SPEECH)
card = config.get(CONF_CARD)
action = config.get(CONF_ACTION)
# pylint: disable=unsubscriptable-object
if speech is not None:
response.add_speech(SpeechType[speech['type']], speech['text'])
if card is not None:
response.add_card(CardType[card['type']], card['title'],
card['content'])
if action is not None:
action.run(response.variables)
return self.json(response)
class SpeechType(enum.Enum):

View File

@@ -6,23 +6,23 @@ https://home-assistant.io/developers/api/
"""
import json
import logging
import re
import threading
from time import time
import homeassistant.core as ha
import homeassistant.remote as rem
from homeassistant.bootstrap import ERROR_LOG_FILENAME
from homeassistant.const import (
CONTENT_TYPE_TEXT_PLAIN, EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_HEADER_CONTENT_TYPE, HTTP_NOT_FOUND,
HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS,
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND,
HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS,
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG,
URL_API_EVENT_FORWARD, URL_API_EVENTS, URL_API_LOG_OUT, URL_API_SERVICES,
URL_API_EVENT_FORWARD, URL_API_EVENTS, URL_API_SERVICES,
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
__version__)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.state import TrackStates
from homeassistant.helpers import template
from homeassistant.components.http import HomeAssistantView
DOMAIN = 'api'
DEPENDENCIES = ['http']
@@ -35,372 +35,365 @@ _LOGGER = logging.getLogger(__name__)
def setup(hass, config):
"""Register the API with the HTTP interface."""
# /api - for validation purposes
hass.http.register_path('GET', URL_API, _handle_get_api)
# /api/config
hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config)
# /api/discovery_info
hass.http.register_path('GET', URL_API_DISCOVERY_INFO,
_handle_get_api_discovery_info,
require_auth=False)
# /api/stream
hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream)
# /api/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)
hass.http.register_path(
'DELETE', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_handle_delete_state_entity)
# /api/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)
# /api/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)
# /api/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)
# /api/components
hass.http.register_path(
'GET', URL_API_COMPONENTS, _handle_get_api_components)
# /api/error_log
hass.http.register_path('GET', URL_API_ERROR_LOG,
_handle_get_api_error_log)
hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out)
# /api/template
hass.http.register_path('POST', URL_API_TEMPLATE,
_handle_post_api_template)
hass.wsgi.register_view(APIStatusView)
hass.wsgi.register_view(APIEventStream)
hass.wsgi.register_view(APIConfigView)
hass.wsgi.register_view(APIDiscoveryView)
hass.wsgi.register_view(APIStatesView)
hass.wsgi.register_view(APIEntityStateView)
hass.wsgi.register_view(APIEventListenersView)
hass.wsgi.register_view(APIEventView)
hass.wsgi.register_view(APIServicesView)
hass.wsgi.register_view(APIDomainServicesView)
hass.wsgi.register_view(APIEventForwardingView)
hass.wsgi.register_view(APIComponentsView)
hass.wsgi.register_view(APIErrorLogView)
hass.wsgi.register_view(APITemplateView)
return True
def _handle_get_api(handler, path_match, data):
"""Render 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()
session_id = None
class APIStatusView(HomeAssistantView):
"""View to handle Status requests."""
restrict = data.get('restrict')
if restrict:
restrict = restrict.split(',')
url = URL_API
name = "api:status"
def write_message(payload):
"""Write a message to the output."""
with write_lock:
msg = "data: {}\n\n".format(payload)
def get(self, request):
"""Retrieve if API is running."""
return self.json_message('API running.')
try:
wfile.write(msg.encode("UTF-8"))
wfile.flush()
except (IOError, ValueError):
# IOError: socket errors
# ValueError: raised when 'I/O operation on closed file'
block.set()
def forward_events(event):
"""Forward events to the open request."""
nonlocal gracefully_closed
class APIEventStream(HomeAssistantView):
"""View to handle EventStream requests."""
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
url = URL_API_STREAM
name = "api:stream"
handler.server.sessions.extend_validation(session_id)
write_message(json.dumps(event, cls=rem.JSONEncoder))
def get(self, request):
"""Provide a streaming interface for the event bus."""
from eventlet.queue import LightQueue, Empty
import eventlet
handler.send_response(HTTP_OK)
handler.send_header('Content-type', 'text/event-stream')
session_id = handler.set_session_cookie_header()
handler.end_headers()
cur_hub = eventlet.hubs.get_hub()
request.environ['eventlet.minimum_write_chunk_size'] = 0
to_write = LightQueue()
stop_obj = object()
if restrict:
for event in restrict:
hass.bus.listen(event, forward_events)
else:
hass.bus.listen(MATCH_ALL, forward_events)
restrict = request.args.get('restrict')
if restrict:
restrict = restrict.split(',')
while True:
write_message(STREAM_PING_PAYLOAD)
def thread_forward_events(event):
"""Forward events to the open request."""
if event.event_type == EVENT_TIME_CHANGED:
return
block.wait(STREAM_PING_INTERVAL)
if restrict and event.event_type not in restrict:
return
if block.is_set():
break
_LOGGER.debug('STREAM %s FORWARDING %s', id(stop_obj), event)
if not gracefully_closed:
_LOGGER.info("Found broken event stream to %s, cleaning up",
handler.client_address[0])
if event.event_type == EVENT_HOMEASSISTANT_STOP:
data = stop_obj
else:
data = json.dumps(event, cls=rem.JSONEncoder)
if restrict:
for event in restrict:
hass.bus.remove_listener(event, forward_events)
else:
hass.bus.remove_listener(MATCH_ALL, forward_events)
cur_hub.schedule_call_global(0, lambda: to_write.put(data))
def stream():
"""Stream events to response."""
self.hass.bus.listen(MATCH_ALL, thread_forward_events)
def _handle_get_api_config(handler, path_match, data):
"""Return the Home Assistant configuration."""
handler.write_json(handler.server.hass.config.as_dict())
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
last_msg = time()
# Fire off one message right away to have browsers fire open event
to_write.put(STREAM_PING_PAYLOAD)
def _handle_get_api_discovery_info(handler, path_match, data):
needs_auth = (handler.server.hass.config.api.api_password is not None)
params = {
'base_url': handler.server.hass.config.api.base_url,
'location_name': handler.server.hass.config.location_name,
'requires_api_password': needs_auth,
'version': __version__
}
handler.write_json(params)
while True:
try:
# Somehow our queue.get sometimes takes too long to
# be notified of arrival of data. Probably
# because of our spawning on hub in other thread
# hack. Because current goal is to get this out,
# We just timeout every second because it will
# return right away if qsize() > 0.
# So yes, we're basically polling :(
payload = to_write.get(timeout=1)
if payload is stop_obj:
break
def _handle_get_api_states(handler, path_match, data):
"""Return a dict containing all entity ids and their state."""
handler.write_json(handler.server.hass.states.all())
msg = "data: {}\n\n".format(payload)
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
msg.strip())
yield msg.encode("UTF-8")
last_msg = time()
except Empty:
if time() - last_msg > 50:
to_write.put(STREAM_PING_PAYLOAD)
except GeneratorExit:
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
break
self.hass.bus.remove_listener(MATCH_ALL, thread_forward_events)
def _handle_get_api_states_entity(handler, path_match, data):
"""Return the state of a specific entity."""
entity_id = path_match.group('entity_id')
return self.Response(stream(), mimetype='text/event-stream')
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)
class APIConfigView(HomeAssistantView):
"""View to handle Config requests."""
url = URL_API_CONFIG
name = "api:config"
def _handle_post_state_entity(handler, path_match, data):
"""Handle updating the state of an entity.
def get(self, request):
"""Get current configuration."""
return self.json(self.hass.config.as_dict())
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
class APIDiscoveryView(HomeAssistantView):
"""View to provide discovery info."""
attributes = data['attributes'] if 'attributes' in data else None
requires_auth = False
url = URL_API_DISCOVERY_INFO
name = "api:discovery"
is_new_state = handler.server.hass.states.get(entity_id) is None
def get(self, request):
"""Get discovery info."""
needs_auth = self.hass.config.api.api_password is not None
return self.json({
'base_url': self.hass.config.api.base_url,
'location_name': self.hass.config.location_name,
'requires_api_password': needs_auth,
'version': __version__
})
# Write state
handler.server.hass.states.set(entity_id, new_state, attributes)
state = handler.server.hass.states.get(entity_id)
class APIStatesView(HomeAssistantView):
"""View to handle States requests."""
status_code = HTTP_CREATED if is_new_state else HTTP_OK
url = URL_API_STATES
name = "api:states"
handler.write_json(
state.as_dict(),
status_code=status_code,
location=URL_API_STATES_ENTITY.format(entity_id))
def get(self, request):
"""Get current states."""
return self.json(self.hass.states.all())
def _handle_delete_state_entity(handler, path_match, data):
"""Handle request to delete an entity from state machine.
class APIEntityStateView(HomeAssistantView):
"""View to handle EntityState requests."""
This handles the following paths:
/api/states/<entity_id>
"""
entity_id = path_match.group('entity_id')
url = "/api/states/<entity(exist=False):entity_id>"
name = "api:entity-state"
if handler.server.hass.states.remove(entity_id):
handler.write_json_message(
"Entity not found", HTTP_NOT_FOUND)
else:
handler.write_json_message(
"Entity removed", HTTP_OK)
def get(self, request, entity_id):
"""Retrieve state of entity."""
state = self.hass.states.get(entity_id)
if state:
return self.json(state)
else:
return self.json_message('Entity not found', HTTP_NOT_FOUND)
def post(self, request, entity_id):
"""Update state of entity."""
try:
new_state = request.json['state']
except KeyError:
return self.json_message('No state specified', HTTP_BAD_REQUEST)
def _handle_get_api_events(handler, path_match, data):
"""Handle getting overview of event listeners."""
handler.write_json(events_json(handler.server.hass))
attributes = request.json.get('attributes')
is_new_state = self.hass.states.get(entity_id) is None
def _handle_api_post_events_event(handler, path_match, event_data):
"""Handle firing of an event.
# Write state
self.hass.states.set(entity_id, new_state, attributes)
This handles the following paths: /api/events/<event_type>
# Read the state back for our response
resp = self.json(self.hass.states.get(entity_id))
Events from /api are threated as remote events.
"""
event_type = path_match.group('event_type')
if is_new_state:
resp.status_code = HTTP_CREATED
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)
return
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
event_origin = ha.EventOrigin.remote
return resp
# 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))
def delete(self, request, entity_id):
"""Remove entity."""
if self.hass.states.remove(entity_id):
return self.json_message('Entity removed')
else:
return self.json_message('Entity not found', HTTP_NOT_FOUND)
if state:
event_data[key] = state
handler.server.hass.bus.fire(event_type, event_data, event_origin)
class APIEventListenersView(HomeAssistantView):
"""View to handle EventListeners requests."""
handler.write_json_message("Event {} fired.".format(event_type))
url = URL_API_EVENTS
name = "api:event-listeners"
def get(self, request):
"""Get event listeners."""
return self.json(events_json(self.hass))
def _handle_get_api_services(handler, path_match, data):
"""Handle getting overview of services."""
handler.write_json(services_json(handler.server.hass))
class APIEventView(HomeAssistantView):
"""View to handle Event requests."""
# pylint: disable=invalid-name
def _handle_post_api_services_domain_service(handler, path_match, data):
"""Handle calling a service.
url = '/api/events/<event_type>'
name = "api:event"
This handles the following paths: /api/services/<domain>/<service>
"""
domain = path_match.group('domain')
service = path_match.group('service')
def post(self, request, event_type):
"""Fire events."""
event_data = request.json
with TrackStates(handler.server.hass) as changed_states:
handler.server.hass.services.call(domain, service, data, True)
if event_data is not None and not isinstance(event_data, dict):
return self.json_message('Event data should be a JSON object',
HTTP_BAD_REQUEST)
handler.write_json(changed_states)
# 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
# pylint: disable=invalid-name
def _handle_post_api_event_forward(handler, path_match, data):
"""Handle 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
self.hass.bus.fire(event_type, event_data, ha.EventOrigin.remote)
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
return self.json_message("Event {} fired.".format(event_type))
api = rem.API(host, api_password, port)
if not api.validate_api():
handler.write_json_message(
"Unable to validate API", HTTP_UNPROCESSABLE_ENTITY)
return
class APIServicesView(HomeAssistantView):
"""View to handle Services requests."""
if handler.server.event_forwarder is None:
handler.server.event_forwarder = \
rem.EventForwarder(handler.server.hass)
url = URL_API_SERVICES
name = "api:services"
handler.server.event_forwarder.connect(api)
def get(self, request):
"""Get registered services."""
return self.json(services_json(self.hass))
handler.write_json_message("Event forwarding setup.")
class APIDomainServicesView(HomeAssistantView):
"""View to handle DomainServices requests."""
def _handle_delete_api_event_forward(handler, path_match, data):
"""Handle deleting an event forwarding target."""
try:
host = data['host']
except KeyError:
handler.write_json_message("No host received.", HTTP_BAD_REQUEST)
return
url = "/api/services/<domain>/<service>"
name = "api:domain-services"
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
def post(self, request, domain, service):
"""Call a service.
if handler.server.event_forwarder is not None:
api = rem.API(host, None, port)
Returns a list of changed states.
"""
with TrackStates(self.hass) as changed_states:
self.hass.services.call(domain, service, request.json, True)
handler.server.event_forwarder.disconnect(api)
return self.json(changed_states)
handler.write_json_message("Event forwarding cancelled.")
class APIEventForwardingView(HomeAssistantView):
"""View to handle EventForwarding requests."""
def _handle_get_api_components(handler, path_match, data):
"""Return all the loaded components."""
handler.write_json(handler.server.hass.config.components)
url = URL_API_EVENT_FORWARD
name = "api:event-forward"
event_forwarder = None
def post(self, request):
"""Setup an event forwarder."""
data = request.json
if data is None:
return self.json_message("No data received.", HTTP_BAD_REQUEST)
try:
host = data['host']
api_password = data['api_password']
except KeyError:
return self.json_message("No host or api_password received.",
HTTP_BAD_REQUEST)
def _handle_get_api_error_log(handler, path_match, data):
"""Return the logged errors for this session."""
handler.write_file(handler.server.hass.config.path(ERROR_LOG_FILENAME),
False)
try:
port = int(data['port']) if 'port' in data else None
except ValueError:
return self.json_message("Invalid value received for port.",
HTTP_UNPROCESSABLE_ENTITY)
api = rem.API(host, api_password, port)
def _handle_post_api_log_out(handler, path_match, data):
"""Log user out."""
handler.send_response(HTTP_OK)
handler.destroy_session()
handler.end_headers()
if not api.validate_api():
return self.json_message("Unable to validate API.",
HTTP_UNPROCESSABLE_ENTITY)
if self.event_forwarder is None:
self.event_forwarder = rem.EventForwarder(self.hass)
def _handle_post_api_template(handler, path_match, data):
"""Log user out."""
template_string = data.get('template', '')
self.event_forwarder.connect(api)
try:
rendered = template.render(handler.server.hass, template_string)
return self.json_message("Event forwarding setup.")
handler.send_response(HTTP_OK)
handler.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
handler.end_headers()
handler.wfile.write(rendered.encode('utf-8'))
except TemplateError as e:
handler.write_json_message(str(e), HTTP_UNPROCESSABLE_ENTITY)
return
def delete(self, request):
"""Remove event forwarer."""
data = request.json
if data is None:
return self.json_message("No data received.", HTTP_BAD_REQUEST)
try:
host = data['host']
except KeyError:
return self.json_message("No host received.", HTTP_BAD_REQUEST)
try:
port = int(data['port']) if 'port' in data else None
except ValueError:
return self.json_message("Invalid value received for port.",
HTTP_UNPROCESSABLE_ENTITY)
if self.event_forwarder is not None:
api = rem.API(host, None, port)
self.event_forwarder.disconnect(api)
return self.json_message("Event forwarding cancelled.")
class APIComponentsView(HomeAssistantView):
"""View to handle Components requests."""
url = URL_API_COMPONENTS
name = "api:components"
def get(self, request):
"""Get current loaded components."""
return self.json(self.hass.config.components)
class APIErrorLogView(HomeAssistantView):
"""View to handle ErrorLog requests."""
url = URL_API_ERROR_LOG
name = "api:error-log"
def get(self, request):
"""Serve error log."""
return self.file(request, self.hass.config.path(ERROR_LOG_FILENAME))
class APITemplateView(HomeAssistantView):
"""View to handle requests."""
url = URL_API_TEMPLATE
name = "api:template"
def post(self, request):
"""Render a template."""
try:
return template.render(self.hass, request.json['template'],
request.json.get('variables'))
except TemplateError as ex:
return self.json_message('Error rendering template: {}'.format(ex),
HTTP_BAD_REQUEST)
def services_json(hass):

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers import condition, config_validation as cv
TRIGGER_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_PLATFORM): 'numeric_state',
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
CONF_BELOW: vol.Coerce(float),
CONF_ABOVE: vol.Coerce(float),
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
@@ -41,7 +41,7 @@ def trigger(hass, config, action):
variables = {
'trigger': {
'platform': 'numeric_state',
'entity_id': entity_id,
'entity_id': entity,
'below': below,
'above': above,
}

View File

@@ -0,0 +1,63 @@
"""
Support for EnOcean binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.enocean/
"""
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components import enocean
from homeassistant.const import CONF_NAME
DEPENDENCIES = ["enocean"]
CONF_ID = "id"
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Binary Sensor platform fo EnOcean."""
dev_id = config.get(CONF_ID, None)
devname = config.get(CONF_NAME, "EnOcean binary sensor")
add_devices([EnOceanBinarySensor(dev_id, devname)])
class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
"""Representation of EnOcean binary sensors such as wall switches."""
def __init__(self, dev_id, devname):
"""Initialize the EnOcean binary sensor."""
enocean.EnOceanDevice.__init__(self)
self.stype = "listener"
self.dev_id = dev_id
self.which = -1
self.onoff = -1
self.devname = devname
@property
def name(self):
"""The default name for the binary sensor."""
return self.devname
def value_changed(self, value, value2):
"""Fire an event with the data that have changed.
This method is called when there is an incoming packet associated
with this platform.
"""
self.update_ha_state()
if value2 == 0x70:
self.which = 0
self.onoff = 0
elif value2 == 0x50:
self.which = 0
self.onoff = 1
elif value2 == 0x30:
self.which = 1
self.onoff = 0
elif value2 == 0x10:
self.which = 1
self.onoff = 1
self.hass.bus.fire('button_pressed', {"id": self.dev_id,
'pushed': value,
'which': self.which,
'onoff': self.onoff})

View File

@@ -9,11 +9,12 @@ import logging
from homeassistant.components.binary_sensor import (BinarySensorDevice,
ENTITY_ID_FORMAT,
SENSOR_CLASSES)
from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE
from homeassistant.core import EVENT_STATE_CHANGED
from homeassistant.const import (ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE,
ATTR_ENTITY_ID, MATCH_ALL)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers import template
from homeassistant.helpers.event import track_state_change
from homeassistant.util import slugify
CONF_SENSORS = 'sensors'
@@ -52,13 +53,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
'Missing %s for sensor %s', CONF_VALUE_TEMPLATE, device)
continue
entity_ids = device_config.get(ATTR_ENTITY_ID, MATCH_ALL)
sensors.append(
BinarySensorTemplate(
hass,
device,
friendly_name,
sensor_class,
value_template)
value_template,
entity_ids)
)
if not sensors:
_LOGGER.error('No sensors added')
@@ -73,7 +77,7 @@ class BinarySensorTemplate(BinarySensorDevice):
# pylint: disable=too-many-arguments
def __init__(self, hass, device, friendly_name, sensor_class,
value_template):
value_template, entity_ids):
"""Initialize the Template binary sensor."""
self.hass = hass
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device,
@@ -85,12 +89,12 @@ class BinarySensorTemplate(BinarySensorDevice):
self.update()
def template_bsensor_event_listener(event):
def template_bsensor_state_listener(entity, old_state, new_state):
"""Called when the target device changes state."""
self.update_ha_state(True)
hass.bus.listen(EVENT_STATE_CHANGED,
template_bsensor_event_listener)
track_state_change(hass, entity_ids,
template_bsensor_state_listener)
@property
def name(self):

View File

@@ -6,17 +6,12 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/camera/
"""
import logging
import re
import time
import requests
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components import bloomsky
from homeassistant.const import HTTP_OK, HTTP_NOT_FOUND, ATTR_ENTITY_ID
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView
DOMAIN = 'camera'
DEPENDENCIES = ['http']
@@ -32,10 +27,7 @@ STATE_RECORDING = 'recording'
STATE_STREAMING = 'streaming'
STATE_IDLE = 'idle'
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}'
MULTIPART_BOUNDARY = '--jpgboundary'
MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n'
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
# pylint: disable=too-many-branches
@@ -45,57 +37,11 @@ def setup(hass, config):
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS)
hass.wsgi.register_view(CameraImageView(hass, component.entities))
hass.wsgi.register_view(CameraMjpegStream(hass, component.entities))
component.setup(config)
def _proxy_camera_image(handler, path_match, data):
"""Serve the camera image via the HA server."""
entity_id = path_match.group(ATTR_ENTITY_ID)
camera = component.entities.get(entity_id)
if camera is None:
handler.send_response(HTTP_NOT_FOUND)
handler.end_headers()
return
response = camera.camera_image()
if response is None:
handler.send_response(HTTP_NOT_FOUND)
handler.end_headers()
return
handler.send_response(HTTP_OK)
handler.write_content(response)
hass.http.register_path(
'GET',
re.compile(r'/api/camera_proxy/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_proxy_camera_image)
def _proxy_camera_mjpeg_stream(handler, path_match, data):
"""Proxy the camera image as an mjpeg stream via the HA server."""
entity_id = path_match.group(ATTR_ENTITY_ID)
camera = component.entities.get(entity_id)
if camera is None:
handler.send_response(HTTP_NOT_FOUND)
handler.end_headers()
return
try:
camera.is_streaming = True
camera.update_ha_state()
camera.mjpeg_stream(handler)
except (requests.RequestException, IOError):
camera.is_streaming = False
camera.update_ha_state()
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
@@ -106,6 +52,11 @@ class Camera(Entity):
"""Initialize a camera."""
self.is_streaming = False
@property
def access_token(self):
"""Access token for this camera."""
return str(id(self))
@property
def should_poll(self):
"""No need to poll cameras."""
@@ -114,7 +65,7 @@ class Camera(Entity):
@property
def entity_picture(self):
"""Return a link to the camera feed as entity picture."""
return ENTITY_IMAGE_URL.format(self.entity_id)
return ENTITY_IMAGE_URL.format(self.entity_id, self.access_token)
@property
def is_recording(self):
@@ -135,32 +86,35 @@ class Camera(Entity):
"""Return bytes of camera image."""
raise NotImplementedError()
def mjpeg_stream(self, handler):
def mjpeg_stream(self, response):
"""Generate an HTTP MJPEG stream from camera images."""
def write_string(text):
"""Helper method to write a string to the stream."""
handler.request.sendall(bytes(text + '\r\n', 'utf-8'))
import eventlet
response.content_type = ('multipart/x-mixed-replace; '
'boundary=--jpegboundary')
write_string('HTTP/1.1 200 OK')
write_string('Content-type: multipart/x-mixed-replace; '
'boundary={}'.format(MULTIPART_BOUNDARY))
write_string('')
write_string(MULTIPART_BOUNDARY)
def stream():
"""Stream images as mjpeg stream."""
try:
last_image = None
while True:
img_bytes = self.camera_image()
while True:
img_bytes = self.camera_image()
if img_bytes is not None and img_bytes != last_image:
yield bytes(
'--jpegboundary\r\n'
'Content-Type: image/jpeg\r\n'
'Content-Length: {}\r\n\r\n'.format(
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n'
if img_bytes is None:
continue
last_image = img_bytes
write_string('Content-length: {}'.format(len(img_bytes)))
write_string('Content-type: image/jpeg')
write_string('')
handler.request.sendall(img_bytes)
write_string('')
write_string(MULTIPART_BOUNDARY)
eventlet.sleep(0.5)
except GeneratorExit:
pass
time.sleep(0.5)
response.response = stream()
return response
@property
def state(self):
@@ -175,7 +129,9 @@ class Camera(Entity):
@property
def state_attributes(self):
"""Camera state attributes."""
attr = {}
attr = {
'access_token': self.access_token,
}
if self.model:
attr['model_name'] = self.model
@@ -184,3 +140,60 @@ class Camera(Entity):
attr['brand'] = self.brand
return attr
class CameraView(HomeAssistantView):
"""Base CameraView."""
requires_auth = False
def __init__(self, hass, entities):
"""Initialize a basic camera view."""
super().__init__(hass)
self.entities = entities
def get(self, request, entity_id):
"""Start a get request."""
camera = self.entities.get(entity_id)
if camera is None:
return self.Response(status=404)
authenticated = (request.authenticated or
request.args.get('token') == camera.access_token)
if not authenticated:
return self.Response(status=401)
return self.handle(camera)
def handle(self, camera):
"""Hanlde the camera request."""
raise NotImplementedError()
class CameraImageView(CameraView):
"""Camera view to serve an image."""
url = "/api/camera_proxy/<entity(domain=camera):entity_id>"
name = "api:camera:image"
def handle(self, camera):
"""Serve camera image."""
response = camera.camera_image()
if response is None:
return self.Response(status=500)
return self.Response(response)
class CameraMjpegStream(CameraView):
"""Camera View to serve an MJPEG stream."""
url = "/api/camera_proxy_stream/<entity(domain=camera):entity_id>"
name = "api:camera:stream"
def handle(self, camera):
"""Serve camera image."""
return camera.mjpeg_stream(self.Response())

View File

@@ -11,7 +11,6 @@ import requests
from requests.auth import HTTPBasicAuth
from homeassistant.components.camera import DOMAIN, Camera
from homeassistant.const import HTTP_OK
from homeassistant.helpers import validate_config
CONTENT_TYPE_HEADER = 'Content-Type'
@@ -68,19 +67,12 @@ class MjpegCamera(Camera):
with closing(self.camera_stream()) as response:
return process_response(response)
def mjpeg_stream(self, handler):
def mjpeg_stream(self, response):
"""Generate an HTTP MJPEG stream from the camera."""
response = self.camera_stream()
content_type = response.headers[CONTENT_TYPE_HEADER]
handler.send_response(HTTP_OK)
handler.send_header(CONTENT_TYPE_HEADER, content_type)
handler.end_headers()
for chunk in response.iter_content(chunk_size=1024):
if not chunk:
break
handler.wfile.write(chunk)
stream = self.camera_stream()
response.mimetype = stream.headers[CONTENT_TYPE_HEADER]
response.response = stream.iter_content(chunk_size=1024)
return response
@property
def name(self):

View File

@@ -12,7 +12,7 @@ import requests
from homeassistant.components.camera import DOMAIN, Camera
from homeassistant.helpers import validate_config
REQUIREMENTS = ['uvcclient==0.8']
REQUIREMENTS = ['uvcclient==0.9.0']
_LOGGER = logging.getLogger(__name__)
@@ -45,13 +45,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.error('Unable to connect to NVR: %s', str(ex))
return False
identifier = nvrconn.server_version >= (3, 2, 0) and 'id' or 'uuid'
# Filter out airCam models, which are not supported in the latest
# version of UnifiVideo and which are EOL by Ubiquiti
cameras = [camera for camera in cameras
if 'airCam' not in nvrconn.get_camera(camera['uuid'])['model']]
cameras = [
camera for camera in cameras
if 'airCam' not in nvrconn.get_camera(camera[identifier])['model']]
add_devices([UnifiVideoCamera(nvrconn,
camera['uuid'],
camera[identifier],
camera['name'])
for camera in cameras])
return True
@@ -110,12 +112,17 @@ class UnifiVideoCamera(Camera):
dict(name=self._name))
password = 'ubnt'
if self._nvr.server_version >= (3, 2, 0):
client_cls = uvc_camera.UVCCameraClientV320
else:
client_cls = uvc_camera.UVCCameraClient
camera = None
for addr in addrs:
try:
camera = uvc_camera.UVCCameraClient(addr,
caminfo['username'],
password)
camera = client_cls(addr,
caminfo['username'],
password)
camera.login()
_LOGGER.debug('Logged into UVC camera %(name)s via %(addr)s',
dict(name=self._name, addr=addr))

View File

@@ -19,6 +19,8 @@ SERVICE_CONFIGURE = "configure"
STATE_CONFIGURE = "configure"
STATE_CONFIGURED = "configured"
ATTR_LINK_NAME = "link_name"
ATTR_LINK_URL = "link_url"
ATTR_CONFIGURE_ID = "configure_id"
ATTR_DESCRIPTION = "description"
ATTR_DESCRIPTION_IMAGE = "description_image"
@@ -34,7 +36,7 @@ _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):
submit_caption=None, fields=None, link_name=None, link_url=None):
"""Create a new request for configuration.
Will return an ID to be used for sequent calls.
@@ -43,7 +45,8 @@ def request_config(
request_id = instance.request_config(
name, callback,
description, description_image, submit_caption, fields)
description, description_image, submit_caption,
fields, link_name, link_url)
_REQUESTS[request_id] = instance
@@ -100,7 +103,8 @@ class Configurator(object):
# pylint: disable=too-many-arguments
def request_config(
self, name, callback,
description, description_image, submit_caption, fields):
description, description_image, submit_caption,
fields, link_name, link_url):
"""Setup a request for configuration."""
entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass)
@@ -121,6 +125,8 @@ class Configurator(object):
(ATTR_DESCRIPTION, description),
(ATTR_DESCRIPTION_IMAGE, description_image),
(ATTR_SUBMIT_CAPTION, submit_caption),
(ATTR_LINK_NAME, link_name),
(ATTR_LINK_URL, link_url),
] if value is not None
})

View File

@@ -27,7 +27,7 @@ SERVICE_PROCESS_SCHEMA = vol.Schema({
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
REQUIREMENTS = ['fuzzywuzzy==0.8.0']
REQUIREMENTS = ['fuzzywuzzy==0.10.0']
def setup(hass, config):

View File

@@ -21,6 +21,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
'camera',
'device_tracker',
'garage_door',
'hvac',
'light',
'lock',
'media_player',

View File

@@ -17,7 +17,7 @@ from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
import homeassistant.util as util
import homeassistant.util.dt as dt_util
@@ -26,6 +26,7 @@ from homeassistant.const import (
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
DOMAIN = "device_tracker"
DEPENDENCIES = ['zone']
@@ -193,7 +194,7 @@ class DeviceTracker(object):
if not device:
dev_id = util.slugify(host_name or '') or util.slugify(mac)
else:
dev_id = str(dev_id).lower()
dev_id = cv.slug(str(dev_id).lower())
device = self.devices.get(dev_id)
if device:

View File

@@ -19,13 +19,16 @@ from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pexpect==4.0.1']
_LEASES_CMD = 'cat /var/lib/misc/dnsmasq.leases'
_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_CMD = 'ip neigh'
_IP_NEIGH_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
r'\w+\s' +
@@ -55,6 +58,7 @@ class AsusWrtDeviceScanner(object):
self.host = config[CONF_HOST]
self.username = str(config[CONF_USERNAME])
self.password = str(config[CONF_PASSWORD])
self.protocol = config.get('protocol')
self.lock = threading.Lock()
@@ -100,8 +104,26 @@ class AsusWrtDeviceScanner(object):
self.last_results = active_clients
return True
def get_asuswrt_data(self):
"""Retrieve data from ASUSWRT and return parsed result."""
def ssh_connection(self):
"""Retrieve data from ASUSWRT via the ssh protocol."""
from pexpect import pxssh
try:
ssh = pxssh.pxssh()
ssh.login(self.host, self.username, self.password)
ssh.sendline(_IP_NEIGH_CMD)
ssh.prompt()
neighbors = ssh.before.split(b'\n')[1:-1]
ssh.sendline(_LEASES_CMD)
ssh.prompt()
leases_result = ssh.before.split(b'\n')[1:-1]
ssh.logout()
return (neighbors, leases_result)
except pxssh.ExceptionPxssh as exc:
_LOGGER.exception('Unexpected response from router: %s', exc)
return ('', '')
def telnet_connection(self):
"""Retrieve data from ASUSWRT via the telnet protocol."""
try:
telnet = telnetlib.Telnet(self.host)
telnet.read_until(b'login: ')
@@ -109,18 +131,26 @@ class AsusWrtDeviceScanner(object):
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'))
telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii'))
neighbors = telnet.read_until(prompt_string).split(b'\n')[1:-1]
telnet.write('cat /var/lib/misc/dnsmasq.leases\n'.encode('ascii'))
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
leases_result = telnet.read_until(prompt_string).split(b'\n')[1:-1]
telnet.write('exit\n'.encode('ascii'))
return (neighbors, leases_result)
except EOFError:
_LOGGER.exception("Unexpected response from router")
return
return ('', '')
except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router," +
_LOGGER.exception("Connection refused by router,"
" is telnet enabled?")
return
return ('', '')
def get_asuswrt_data(self):
"""Retrieve data from ASUSWRT and return parsed result."""
if self.protocol == 'telnet':
neighbors, leases_result = self.telnet_connection()
else:
neighbors, leases_result = self.ssh_connection()
devices = {}
for lease in leases_result:

View File

@@ -5,95 +5,92 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.locative/
"""
import logging
from functools import partial
from homeassistant.components.device_tracker import DOMAIN
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
from homeassistant.components.http import HomeAssistantView
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http']
URL_API_LOCATIVE_ENDPOINT = "/api/locative"
def setup_scanner(hass, config, see):
"""Setup an endpoint for the Locative application."""
# POST would be semantically better, but that currently does not work
# since Locative sends the data as key1=value1&key2=value2
# in the request body, while Home Assistant expects json there.
hass.http.register_path(
'GET', URL_API_LOCATIVE_ENDPOINT,
partial(_handle_get_api_locative, hass, see))
hass.wsgi.register_view(LocativeView(hass, see))
return True
def _handle_get_api_locative(hass, see, handler, path_match, data):
"""Locative message received."""
if not _check_data(handler, data):
return
class LocativeView(HomeAssistantView):
"""View to handle locative requests."""
device = data['device'].replace('-', '')
location_name = data['id'].lower()
direction = data['trigger']
url = "/api/locative"
name = "api:bootstrap"
if direction == 'enter':
see(dev_id=device, location_name=location_name)
handler.write_text("Setting location to {}".format(location_name))
def __init__(self, hass, see):
"""Initialize Locative url endpoints."""
super().__init__(hass)
self.see = see
elif direction == 'exit':
current_state = hass.states.get("{}.{}".format(DOMAIN, device))
def get(self, request):
"""Locative message received as GET."""
return self.post(request)
def post(self, request):
"""Locative message received."""
# pylint: disable=too-many-return-statements
data = request.values
if 'latitude' not in data or 'longitude' not in data:
return ("Latitude and longitude not specified.",
HTTP_UNPROCESSABLE_ENTITY)
if 'device' not in data:
_LOGGER.error("Device id not specified.")
return ("Device id not specified.",
HTTP_UNPROCESSABLE_ENTITY)
if 'id' not in data:
_LOGGER.error("Location id not specified.")
return ("Location id not specified.",
HTTP_UNPROCESSABLE_ENTITY)
if 'trigger' not in data:
_LOGGER.error("Trigger is not specified.")
return ("Trigger is not specified.",
HTTP_UNPROCESSABLE_ENTITY)
device = data['device'].replace('-', '')
location_name = data['id'].lower()
direction = data['trigger']
if direction == 'enter':
self.see(dev_id=device, location_name=location_name)
return "Setting location to {}".format(location_name)
elif direction == 'exit':
current_state = self.hass.states.get(
"{}.{}".format(DOMAIN, device))
if current_state is None or current_state.state == location_name:
self.see(dev_id=device, location_name=STATE_NOT_HOME)
return "Setting location to not home"
else:
# Ignore the message if it is telling us to exit a zone that we
# aren't currently in. This occurs when a zone is entered
# before the previous zone was exited. The enter message will
# be sent first, then the exit message will be sent second.
return 'Ignoring exit from {} (already in {})'.format(
location_name, current_state)
elif direction == 'test':
# In the app, a test message can be sent. Just return something to
# the user to let them know that it works.
return "Received test message."
if current_state is None or current_state.state == location_name:
see(dev_id=device, location_name=STATE_NOT_HOME)
handler.write_text("Setting location to not home")
else:
# Ignore the message if it is telling us to exit a zone that we
# aren't currently in. This occurs when a zone is entered before
# the previous zone was exited. The enter message will be sent
# first, then the exit message will be sent second.
handler.write_text(
'Ignoring exit from {} (already in {})'.format(
location_name, current_state))
elif direction == 'test':
# In the app, a test message can be sent. Just return something to
# the user to let them know that it works.
handler.write_text("Received test message.")
else:
handler.write_text(
"Received unidentified message: {}".format(direction),
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Received unidentified message from Locative: %s",
direction)
def _check_data(handler, data):
"""Check the data."""
if 'latitude' not in data or 'longitude' not in data:
handler.write_text("Latitude and longitude not specified.",
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Latitude and longitude not specified.")
return False
if 'device' not in data:
handler.write_text("Device id not specified.",
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Device id not specified.")
return False
if 'id' not in data:
handler.write_text("Location id not specified.",
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Location id not specified.")
return False
if 'trigger' not in data:
handler.write_text("Trigger is not specified.",
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Trigger is not specified.")
return False
return True
_LOGGER.error("Received unidentified message from Locative: %s",
direction)
return ("Received unidentified message: {}".format(direction),
HTTP_UNPROCESSABLE_ENTITY)

View File

@@ -11,7 +11,7 @@ from collections import defaultdict
import homeassistant.components.mqtt as mqtt
from homeassistant.const import STATE_HOME
from homeassistant.util import convert
from homeassistant.util import convert, slugify
DEPENDENCIES = ['mqtt']
@@ -53,6 +53,12 @@ def setup_scanner(hass, config, see):
'accuracy %s is not met: %s',
data_type, max_gps_accuracy, data)
return None
if convert(data.get('acc'), float, 1.0) == 0.0:
_LOGGER.debug('Skipping %s update because GPS accuracy'
'is zero',
data_type)
return None
return data
def owntracks_location_update(topic, payload, qos):
@@ -91,7 +97,7 @@ def setup_scanner(hass, config, see):
return
# OwnTracks uses - at the start of a beacon zone
# to switch on 'hold mode' - ignore this
location = data['desc'].lstrip("-")
location = slugify(data['desc'].lstrip("-"))
if location.lower() == 'home':
location = STATE_HOME
@@ -180,7 +186,7 @@ def setup_scanner(hass, config, see):
def _parse_see_args(topic, data):
"""Parse the OwnTracks location parameters, into the format see expects."""
parts = topic.split('/')
dev_id = '{}_{}'.format(parts[1], parts[2])
dev_id = slugify('{}_{}'.format(parts[1], parts[2]))
host_name = parts[1]
kwargs = {
'dev_id': dev_id,

View File

@@ -18,7 +18,7 @@ from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pysnmp==4.2.5']
REQUIREMENTS = ['pysnmp==4.3.2']
CONF_COMMUNITY = "community"
CONF_BASEOID = "baseoid"
@@ -72,7 +72,7 @@ class SnmpScanner(object):
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the WAP is up to date.
"""Ensure the information from the device is up to date.
Return boolean if scanning successful.
"""
@@ -88,7 +88,7 @@ class SnmpScanner(object):
return True
def get_snmp_data(self):
"""Fetch MAC addresses from WAP via SNMP."""
"""Fetch MAC addresses from access point via SNMP."""
devices = []
errindication, errstatus, errindex, restable = self.snmp.nextCmd(
@@ -97,9 +97,10 @@ class SnmpScanner(object):
if errindication:
_LOGGER.error("SNMPLIB error: %s", errindication)
return
# pylint: disable=no-member
if errstatus:
_LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(),
errindex and restable[-1][int(errindex)-1] or '?')
errindex and restable[int(errindex) - 1][0] or '?')
return
for resrow in restable:

View File

@@ -15,10 +15,12 @@ from homeassistant.const import (
EVENT_PLATFORM_DISCOVERED)
DOMAIN = "discovery"
REQUIREMENTS = ['netdisco==0.6.6']
REQUIREMENTS = ['netdisco==0.6.7']
SCAN_INTERVAL = 300 # seconds
LOAD_PLATFORM = 'load_platform'
SERVICE_WEMO = 'belkin_wemo'
SERVICE_HUE = 'philips_hue'
SERVICE_CAST = 'google_cast'
@@ -27,6 +29,7 @@ SERVICE_SONOS = 'sonos'
SERVICE_PLEX = 'plex_mediaserver'
SERVICE_SQUEEZEBOX = 'logitech_mediaserver'
SERVICE_PANASONIC_VIERA = 'panasonic_viera'
SERVICE_ROKU = 'roku'
SERVICE_HANDLERS = {
SERVICE_WEMO: "wemo",
@@ -37,6 +40,7 @@ SERVICE_HANDLERS = {
SERVICE_PLEX: 'media_player',
SERVICE_SQUEEZEBOX: 'media_player',
SERVICE_PANASONIC_VIERA: 'media_player',
SERVICE_ROKU: 'media_player',
}
@@ -52,7 +56,7 @@ def listen(hass, service, callback):
def discovery_event_listener(event):
"""Listen for discovery events."""
if event.data[ATTR_SERVICE] in service:
if ATTR_SERVICE in event.data and event.data[ATTR_SERVICE] in service:
callback(event.data[ATTR_SERVICE], event.data.get(ATTR_DISCOVERED))
hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener)
@@ -73,6 +77,32 @@ def discover(hass, service, discovered=None, component=None, hass_config=None):
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, data)
def load_platform(hass, component, platform, info=None, hass_config=None):
"""Helper method for generic platform loading.
This method allows a platform to be loaded dynamically without it being
known at runtime (in the DISCOVERY_PLATFORMS list of the component).
Advantages of using this method:
- Any component & platforms combination can be dynamically added
- A component (i.e. light) does not have to import every component
that can dynamically add a platform (e.g. wemo, wink, insteon_hub)
- Custom user components can take advantage of discovery/loading
Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be
fired to load the platform. The event will contain:
{ ATTR_SERVICE = LOAD_PLATFORM + '.' + <<component>>
ATTR_DISCOVERED = {LOAD_PLATFORM: <<platform>>} }
* dev note: This listener can be found in entity_component.py
"""
if info is None:
info = {LOAD_PLATFORM: platform}
else:
info[LOAD_PLATFORM] = platform
discover(hass, LOAD_PLATFORM + '.' + component, info, component,
hass_config)
def setup(hass, config):
"""Start a discovery service."""
logger = logging.getLogger(__name__)

View File

@@ -22,7 +22,7 @@ HOLD_TEMP = 'hold_temp'
REQUIREMENTS = [
'https://github.com/nkgilley/python-ecobee-api/archive/'
'92a2f330cbaf601d0618456fdd97e5a8c42c1c47.zip#python-ecobee==0.0.4']
'4a884bc146a93991b4210f868f3d6aecf0a181e6.zip#python-ecobee==0.0.5']
_LOGGER = logging.getLogger(__name__)

View File

@@ -0,0 +1,117 @@
"""
EnOcean Component.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/EnOcean/
"""
DOMAIN = "enocean"
REQUIREMENTS = ['enocean==0.31']
CONF_DEVICE = "device"
ENOCEAN_DONGLE = None
def setup(hass, config):
"""Setup the EnOcean component."""
global ENOCEAN_DONGLE
serial_dev = config[DOMAIN].get(CONF_DEVICE, "/dev/ttyUSB0")
ENOCEAN_DONGLE = EnOceanDongle(hass, serial_dev)
return True
class EnOceanDongle:
"""Representation of an EnOcean dongle."""
def __init__(self, hass, ser):
"""Initialize the EnOcean dongle."""
from enocean.communicators.serialcommunicator import SerialCommunicator
self.__communicator = SerialCommunicator(port=ser,
callback=self.callback)
self.__communicator.start()
self.__devices = []
def register_device(self, dev):
"""Register another device."""
self.__devices.append(dev)
def send_command(self, command):
"""Send a command from the EnOcean dongle."""
self.__communicator.send(command)
def _combine_hex(self, data): # pylint: disable=no-self-use
"""Combine list of integer values to one big integer."""
output = 0x00
for i, j in enumerate(reversed(data)):
output |= (j << i * 8)
return output
# pylint: disable=too-many-branches
def callback(self, temp):
"""Callback function for EnOcean Device.
This is the callback function called by
python-enocan whenever there is an incoming
packet.
"""
from enocean.protocol.packet import RadioPacket
if isinstance(temp, RadioPacket):
rxtype = None
value = None
if temp.data[6] == 0x30:
rxtype = "wallswitch"
value = 1
elif temp.data[6] == 0x20:
rxtype = "wallswitch"
value = 0
elif temp.data[4] == 0x0c:
rxtype = "power"
value = temp.data[3] + (temp.data[2] << 8)
elif temp.data[2] == 0x60:
rxtype = "switch_status"
if temp.data[3] == 0xe4:
value = 1
elif temp.data[3] == 0x80:
value = 0
elif temp.data[0] == 0xa5 and temp.data[1] == 0x02:
rxtype = "dimmerstatus"
value = temp.data[2]
for device in self.__devices:
if rxtype == "wallswitch" and device.stype == "listener":
if temp.sender == self._combine_hex(device.dev_id):
device.value_changed(value, temp.data[1])
if rxtype == "power" and device.stype == "powersensor":
if temp.sender == self._combine_hex(device.dev_id):
device.value_changed(value)
if rxtype == "power" and device.stype == "switch":
if temp.sender == self._combine_hex(device.dev_id):
if value > 10:
device.value_changed(1)
if rxtype == "switch_status" and device.stype == "switch":
if temp.sender == self._combine_hex(device.dev_id):
device.value_changed(value)
if rxtype == "dimmerstatus" and device.stype == "dimmer":
if temp.sender == self._combine_hex(device.dev_id):
device.value_changed(value)
# pylint: disable=too-few-public-methods
class EnOceanDevice():
"""Parent class for all devices associated with the EnOcean component."""
def __init__(self):
"""Initialize the device."""
ENOCEAN_DONGLE.register_device(self)
self.stype = ""
self.sensorid = [0x00, 0x00, 0x00, 0x00]
# pylint: disable=no-self-use
def send_command(self, data, optional, packet_type):
"""Send a command via the EnOcean dongle."""
from enocean.protocol.packet import Packet
packet = Packet(packet_type, data=data, optional=optional)
ENOCEAN_DONGLE.send_command(packet)

View File

@@ -6,7 +6,11 @@ https://home-assistant.io/components/feedreader/
"""
from datetime import datetime
from logging import getLogger
from os.path import exists
from threading import Lock
import pickle
import voluptuous as vol
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.helpers.event import track_utc_time_change
@@ -27,14 +31,15 @@ MAX_ENTRIES = 20
class FeedManager(object):
"""Abstraction over feedparser module."""
def __init__(self, url, hass):
def __init__(self, url, hass, storage):
"""Initialize the FeedManager object, poll every hour."""
self._url = url
self._feed = None
self._hass = hass
self._firstrun = True
# Initialize last entry timestamp as epoch time
self._last_entry_timestamp = datetime.utcfromtimestamp(0).timetuple()
self._storage = storage
self._last_entry_timestamp = None
self._has_published_parsed = False
hass.bus.listen_once(EVENT_HOMEASSISTANT_START,
lambda _: self._update())
track_utc_time_change(hass, lambda now: self._update(),
@@ -42,7 +47,7 @@ class FeedManager(object):
def _log_no_entries(self):
"""Send no entries log at debug level."""
_LOGGER.debug('No new entries in feed "%s"', self._url)
_LOGGER.debug('No new entries to be published in feed "%s"', self._url)
def _update(self):
"""Update the feed and publish new entries to the event bus."""
@@ -65,10 +70,13 @@ class FeedManager(object):
len(self._feed.entries),
self._url)
if len(self._feed.entries) > MAX_ENTRIES:
_LOGGER.debug('Publishing only the first %s entries '
_LOGGER.debug('Processing only the first %s entries '
'in feed "%s"', MAX_ENTRIES, self._url)
self._feed.entries = self._feed.entries[0:MAX_ENTRIES]
self._publish_new_entries()
if self._has_published_parsed:
self._storage.put_timestamp(self._url,
self._last_entry_timestamp)
else:
self._log_no_entries()
_LOGGER.info('Fetch from feed "%s" completed', self._url)
@@ -79,9 +87,11 @@ class FeedManager(object):
# let's make use of it to publish only new available
# entries since the last run
if 'published_parsed' in entry.keys():
self._has_published_parsed = True
self._last_entry_timestamp = max(entry.published_parsed,
self._last_entry_timestamp)
else:
self._has_published_parsed = False
_LOGGER.debug('No `published_parsed` info available '
'for entry "%s"', entry.title)
entry.update({'feed_url': self._url})
@@ -90,6 +100,13 @@ class FeedManager(object):
def _publish_new_entries(self):
"""Publish new entries to the event bus."""
new_entries = False
self._last_entry_timestamp = self._storage.get_timestamp(self._url)
if self._last_entry_timestamp:
self._firstrun = False
else:
# Set last entry timestamp as epoch time if not available
self._last_entry_timestamp = \
datetime.utcfromtimestamp(0).timetuple()
for entry in self._feed.entries:
if self._firstrun or (
'published_parsed' in entry.keys() and
@@ -103,8 +120,55 @@ class FeedManager(object):
self._firstrun = False
class StoredData(object):
"""Abstraction over pickle data storage."""
def __init__(self, data_file):
"""Initialize pickle data storage."""
self._data_file = data_file
self._lock = Lock()
self._cache_outdated = True
self._data = {}
self._fetch_data()
def _fetch_data(self):
"""Fetch data stored into pickle file."""
if self._cache_outdated and exists(self._data_file):
try:
_LOGGER.debug('Fetching data from file %s', self._data_file)
with self._lock, open(self._data_file, 'rb') as myfile:
self._data = pickle.load(myfile) or {}
self._cache_outdated = False
# pylint: disable=bare-except
except:
_LOGGER.error('Error loading data from pickled file %s',
self._data_file)
def get_timestamp(self, url):
"""Return stored timestamp for given url."""
self._fetch_data()
return self._data.get(url)
def put_timestamp(self, url, timestamp):
"""Update timestamp for given url."""
self._fetch_data()
with self._lock, open(self._data_file, 'wb') as myfile:
self._data.update({url: timestamp})
_LOGGER.debug('Overwriting feed "%s" timestamp in storage file %s',
url, self._data_file)
try:
pickle.dump(self._data, myfile)
# pylint: disable=bare-except
except:
_LOGGER.error('Error saving pickled data to %s',
self._data_file)
self._cache_outdated = True
def setup(hass, config):
"""Setup the feedreader component."""
urls = config.get(DOMAIN)['urls']
feeds = [FeedManager(url, hass) for url in urls]
data_file = hass.config.path("{}.pickle".format(DOMAIN))
storage = StoredData(data_file)
feeds = [FeedManager(url, hass, storage) for url in urls]
return len(feeds) > 0

View File

@@ -1,121 +1,101 @@
"""Handle the frontend for Home Assistant."""
import re
import os
import logging
from . import version, mdi_version
import homeassistant.util as util
from homeassistant.const import URL_ROOT, HTTP_OK
from homeassistant.components import api
from homeassistant.components.http import HomeAssistantView
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', '/map', '/devService', '/devState',
'/devEvent', '/devInfo', '/devTemplate',
re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)'),
]
URL_API_BOOTSTRAP = "/api/bootstrap"
_FINGERPRINT = re.compile(r'^(\w+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
def setup(hass, config):
"""Setup serving the frontend."""
for url in FRONTEND_URLS:
hass.http.register_path('GET', url, _handle_get_root, False)
hass.wsgi.register_view(IndexView)
hass.wsgi.register_view(BootstrapView)
hass.http.register_path('GET', '/service_worker.js',
_handle_get_service_worker, False)
# Bootstrap API
hass.http.register_path(
'GET', URL_API_BOOTSTRAP, _handle_get_api_bootstrap)
# 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)
hass.http.register_path(
'GET', re.compile(r'/local/(?P<file>[a-zA-Z\._\-0-9/]+)'),
_handle_get_local, False)
return True
def _handle_get_api_bootstrap(handler, path_match, data):
"""Return all data needed to bootstrap Home Assistant."""
hass = handler.server.hass
handler.write_json({
'config': hass.config.as_dict(),
'states': hass.states.all(),
'events': api.events_json(hass),
'services': api.services_json(hass),
})
def _handle_get_root(handler, path_match, data):
"""Render the frontend."""
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.api_password is None
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)
template_html = template_html.replace('{{ icons }}', mdi_version.VERSION)
handler.send_response(HTTP_OK)
handler.write_content(template_html.encode("UTF-8"),
'text/html; charset=utf-8')
def _handle_get_service_worker(handler, path_match, data):
"""Return service worker for the frontend."""
if handler.server.development:
www_static_path = os.path.join(os.path.dirname(__file__), 'www_static')
if hass.wsgi.development:
sw_path = "home-assistant-polymer/build/service_worker.js"
else:
sw_path = "service_worker.js"
handler.write_file(os.path.join(os.path.dirname(__file__), 'www_static',
sw_path))
hass.wsgi.register_static_path(
"/service_worker.js",
os.path.join(www_static_path, sw_path),
0
)
hass.wsgi.register_static_path(
"/robots.txt",
os.path.join(www_static_path, "robots.txt")
)
hass.wsgi.register_static_path("/static", www_static_path)
hass.wsgi.register_static_path("/local", hass.config.path('www'))
return True
def _handle_get_static(handler, path_match, data):
"""Return a static file for the frontend."""
req_file = util.sanitize_path(path_match.group('file'))
class BootstrapView(HomeAssistantView):
"""View to bootstrap frontend with all needed data."""
# Strip md5 hash out
fingerprinted = _FINGERPRINT.match(req_file)
if fingerprinted:
req_file = "{}.{}".format(*fingerprinted.groups())
url = "/api/bootstrap"
name = "api:bootstrap"
path = os.path.join(os.path.dirname(__file__), 'www_static', req_file)
handler.write_file(path)
def get(self, request):
"""Return all data needed to bootstrap Home Assistant."""
return self.json({
'config': self.hass.config.as_dict(),
'states': self.hass.states.all(),
'events': api.events_json(self.hass),
'services': api.services_json(self.hass),
})
def _handle_get_local(handler, path_match, data):
"""Return a static file from the hass.config.path/www for the frontend."""
req_file = util.sanitize_path(path_match.group('file'))
class IndexView(HomeAssistantView):
"""Serve the frontend."""
path = handler.server.hass.config.path('www', req_file)
url = '/'
name = "frontend:index"
requires_auth = False
extra_urls = ['/logbook', '/history', '/map', '/devService', '/devState',
'/devEvent', '/devInfo', '/devTemplate',
'/states', '/states/<entity:entity_id>']
handler.write_file(path)
def __init__(self, hass):
"""Initialize the frontend view."""
super().__init__(hass)
from jinja2 import FileSystemLoader, Environment
self.templates = Environment(
loader=FileSystemLoader(
os.path.join(os.path.dirname(__file__), 'templates/')
)
)
def get(self, request, entity_id=None):
"""Serve the index view."""
if self.hass.wsgi.development:
core_url = 'home-assistant-polymer/build/_core_compiled.js'
ui_url = 'home-assistant-polymer/src/home-assistant.html'
else:
core_url = 'core-{}.js'.format(version.CORE)
ui_url = 'frontend-{}.html'.format(version.UI)
# auto login if no password was set
if self.hass.config.api.api_password is None:
auth = 'true'
else:
auth = 'false'
icons_url = 'mdi-{}.html'.format(mdi_version.VERSION)
template = self.templates.get_template('index.html')
# pylint is wrong
# pylint: disable=no-member
resp = template.render(
core_url=core_url, ui_url=ui_url, auth=auth,
icons_url=icons_url, icons=mdi_version.VERSION)
return self.Response(resp, mimetype='text/html')

View File

@@ -1,2 +1,2 @@
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
VERSION = "1baebe8155deb447230866d7ae854bd9"
VERSION = "9ee3d4466a65bef35c2c8974e91b37c0"

View File

@@ -9,6 +9,11 @@
<link rel='apple-touch-icon' sizes='180x180'
href='/static/favicon-apple-180x180.png'>
<meta name='apple-mobile-web-app-capable' content='yes'>
<meta name="msapplication-square70x70logo" content="/static/tile-win-70x70.png"/>
<meta name="msapplication-square150x150logo" content="/static/tile-win-150x150.png"/>
<meta name="msapplication-wide310x150logo" content="/static/tile-win-310x150.png"/>
<meta name="msapplication-square310x310logo" content="/static/tile-win-310x310.png"/>
<meta name="msapplication-TileColor" content="#3fbbf4ff"/>
<meta name='mobile-web-app-capable' content='yes'>
<meta name='viewport' content='width=device-width, user-scalable=no'>
<meta name='theme-color' content='#03a9f4'>
@@ -28,7 +33,7 @@
left: 0;
right: 0;
bottom: 0;
margin-bottom: 97px;
margin-bottom: 83px;
font-family: Roboto, sans-serif;
font-size: 0pt;
transition: font-size 2s;
@@ -36,6 +41,7 @@
#ha-init-skeleton paper-spinner {
height: 28px;
margin-top: 16px;
}
#ha-init-skeleton a {
@@ -59,8 +65,8 @@
.getElementById('ha-init-skeleton')
.classList.add('error');
}
window.noAuth = {{ auth }}
</script>
<link rel='import' href='/static/{{ app_url }}' onerror='initError()' async>
</head>
<body fullbleed>
<div id='ha-init-skeleton'>
@@ -68,6 +74,10 @@
<paper-spinner active></paper-spinner>
Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a>
</div>
<home-assistant icons='{{ icons }}'></home-assistant>
<script src='/static/{{ core_url }}'></script>
<link rel='import' href='/static/{{ ui_url }}' onerror='initError()' async>
<link rel='import' href='/static/{{ icons_url }}' async>
<script>
var webComponentsSupported = (
'registerElement' in document &&
@@ -81,6 +91,5 @@
document.head.appendChild(script)
}
</script>
<home-assistant auth='{{ auth }}' icons='{{ icons }}'></home-assistant>
</body>
</html>

View File

@@ -1,2 +1,3 @@
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
VERSION = "77c51c270b0241ce7ba0d1df2d254d6f"
CORE = "d0b415dac66c8056d81380b258af5767"
UI = "b0ea2672fff86b1ab86dd86135d4b43a"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,12 +8,12 @@
{
"src": "/static/favicon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"type": "image/png"
},
{
"src": "/static/favicon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"type": "image/png"
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -1 +1,258 @@
!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={i:r,l:!1,exports:{}};return e[r].call(s.exports,s,s.exports,t),s.l=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(t.s=192)}({192:function(e,t,n){var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){return n||fetch(e.request).then(function(e){return t.put(s,e.clone()),e})})}))})}});
/**
* Copyright 2016 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// This generated service worker JavaScript will precache your site's resources.
// The code needs to be saved in a .js file at the top-level of your site, and registered
// from your pages in order to be used. See
// https://github.com/googlechrome/sw-precache/blob/master/demo/app/js/service-worker-registration.js
// for an example of how you can register this script and handle various service worker events.
/* eslint-env worker, serviceworker */
/* eslint-disable indent, no-unused-vars, no-multiple-empty-lines, max-nested-callbacks, space-before-function-paren */
'use strict';
/* eslint-disable quotes, comma-spacing */
var PrecacheConfig = [["/","69818e2c5b6f4ca764c46ac78d2fea04"],["/devEvent","69818e2c5b6f4ca764c46ac78d2fea04"],["/devInfo","69818e2c5b6f4ca764c46ac78d2fea04"],["/devService","69818e2c5b6f4ca764c46ac78d2fea04"],["/devState","69818e2c5b6f4ca764c46ac78d2fea04"],["/devTemplate","69818e2c5b6f4ca764c46ac78d2fea04"],["/history","69818e2c5b6f4ca764c46ac78d2fea04"],["/logbook","69818e2c5b6f4ca764c46ac78d2fea04"],["/map","69818e2c5b6f4ca764c46ac78d2fea04"],["/states","69818e2c5b6f4ca764c46ac78d2fea04"],["/static/core-d0b415dac66c8056d81380b258af5767.js","dfafa8e9e34f53e8c36dd8b3f7299b2a"],["/static/frontend-b0ea2672fff86b1ab86dd86135d4b43a.html","69818e2c5b6f4ca764c46ac78d2fea04"],["/static/mdi-9ee3d4466a65bef35c2c8974e91b37c0.html","9a6846935116cd29279c91e0ee0a26d0"],["static/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]];
/* eslint-enable quotes, comma-spacing */
var CacheNamePrefix = 'sw-precache-v1--' + (self.registration ? self.registration.scope : '') + '-';
var IgnoreUrlParametersMatching = [/^utm_/];
var addDirectoryIndex = function (originalUrl, index) {
var url = new URL(originalUrl);
if (url.pathname.slice(-1) === '/') {
url.pathname += index;
}
return url.toString();
};
var getCacheBustedUrl = function (url, param) {
param = param || Date.now();
var urlWithCacheBusting = new URL(url);
urlWithCacheBusting.search += (urlWithCacheBusting.search ? '&' : '') +
'sw-precache=' + param;
return urlWithCacheBusting.toString();
};
var isPathWhitelisted = function (whitelist, absoluteUrlString) {
// If the whitelist is empty, then consider all URLs to be whitelisted.
if (whitelist.length === 0) {
return true;
}
// Otherwise compare each path regex to the path of the URL passed in.
var path = (new URL(absoluteUrlString)).pathname;
return whitelist.some(function(whitelistedPathRegex) {
return path.match(whitelistedPathRegex);
});
};
var populateCurrentCacheNames = function (precacheConfig,
cacheNamePrefix, baseUrl) {
var absoluteUrlToCacheName = {};
var currentCacheNamesToAbsoluteUrl = {};
precacheConfig.forEach(function(cacheOption) {
var absoluteUrl = new URL(cacheOption[0], baseUrl).toString();
var cacheName = cacheNamePrefix + absoluteUrl + '-' + cacheOption[1];
currentCacheNamesToAbsoluteUrl[cacheName] = absoluteUrl;
absoluteUrlToCacheName[absoluteUrl] = cacheName;
});
return {
absoluteUrlToCacheName: absoluteUrlToCacheName,
currentCacheNamesToAbsoluteUrl: currentCacheNamesToAbsoluteUrl
};
};
var stripIgnoredUrlParameters = function (originalUrl,
ignoreUrlParametersMatching) {
var url = new URL(originalUrl);
url.search = url.search.slice(1) // Exclude initial '?'
.split('&') // Split into an array of 'key=value' strings
.map(function(kv) {
return kv.split('='); // Split each 'key=value' string into a [key, value] array
})
.filter(function(kv) {
return ignoreUrlParametersMatching.every(function(ignoredRegex) {
return !ignoredRegex.test(kv[0]); // Return true iff the key doesn't match any of the regexes.
});
})
.map(function(kv) {
return kv.join('='); // Join each [key, value] array into a 'key=value' string
})
.join('&'); // Join the array of 'key=value' strings into a string with '&' in between each
return url.toString();
};
var mappings = populateCurrentCacheNames(PrecacheConfig, CacheNamePrefix, self.location);
var AbsoluteUrlToCacheName = mappings.absoluteUrlToCacheName;
var CurrentCacheNamesToAbsoluteUrl = mappings.currentCacheNamesToAbsoluteUrl;
function deleteAllCaches() {
return caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
return caches.delete(cacheName);
})
);
});
}
self.addEventListener('install', function(event) {
event.waitUntil(
// Take a look at each of the cache names we expect for this version.
Promise.all(Object.keys(CurrentCacheNamesToAbsoluteUrl).map(function(cacheName) {
return caches.open(cacheName).then(function(cache) {
// Get a list of all the entries in the specific named cache.
// For caches that are already populated for a given version of a
// resource, there should be 1 entry.
return cache.keys().then(function(keys) {
// If there are 0 entries, either because this is a brand new version
// of a resource or because the install step was interrupted the
// last time it ran, then we need to populate the cache.
if (keys.length === 0) {
// Use the last bit of the cache name, which contains the hash,
// as the cache-busting parameter.
// See https://github.com/GoogleChrome/sw-precache/issues/100
var cacheBustParam = cacheName.split('-').pop();
var urlWithCacheBusting = getCacheBustedUrl(
CurrentCacheNamesToAbsoluteUrl[cacheName], cacheBustParam);
var request = new Request(urlWithCacheBusting,
{credentials: 'same-origin'});
return fetch(request).then(function(response) {
if (response.ok) {
return cache.put(CurrentCacheNamesToAbsoluteUrl[cacheName],
response);
}
console.error('Request for %s returned a response status %d, ' +
'so not attempting to cache it.',
urlWithCacheBusting, response.status);
// Get rid of the empty cache if we can't add a successful response to it.
return caches.delete(cacheName);
});
}
});
});
})).then(function() {
return caches.keys().then(function(allCacheNames) {
return Promise.all(allCacheNames.filter(function(cacheName) {
return cacheName.indexOf(CacheNamePrefix) === 0 &&
!(cacheName in CurrentCacheNamesToAbsoluteUrl);
}).map(function(cacheName) {
return caches.delete(cacheName);
})
);
});
}).then(function() {
if (typeof self.skipWaiting === 'function') {
// Force the SW to transition from installing -> active state
self.skipWaiting();
}
})
);
});
if (self.clients && (typeof self.clients.claim === 'function')) {
self.addEventListener('activate', function(event) {
event.waitUntil(self.clients.claim());
});
}
self.addEventListener('message', function(event) {
if (event.data.command === 'delete_all') {
console.log('About to delete all caches...');
deleteAllCaches().then(function() {
console.log('Caches deleted.');
event.ports[0].postMessage({
error: null
});
}).catch(function(error) {
console.log('Caches not deleted:', error);
event.ports[0].postMessage({
error: error
});
});
}
});
self.addEventListener('fetch', function(event) {
if (event.request.method === 'GET') {
var urlWithoutIgnoredParameters = stripIgnoredUrlParameters(event.request.url,
IgnoreUrlParametersMatching);
var cacheName = AbsoluteUrlToCacheName[urlWithoutIgnoredParameters];
var directoryIndex = 'index.html';
if (!cacheName && directoryIndex) {
urlWithoutIgnoredParameters = addDirectoryIndex(urlWithoutIgnoredParameters, directoryIndex);
cacheName = AbsoluteUrlToCacheName[urlWithoutIgnoredParameters];
}
var navigateFallback = '';
// Ideally, this would check for event.request.mode === 'navigate', but that is not widely
// supported yet:
// https://code.google.com/p/chromium/issues/detail?id=540967
// https://bugzilla.mozilla.org/show_bug.cgi?id=1209081
if (!cacheName && navigateFallback && event.request.headers.has('accept') &&
event.request.headers.get('accept').includes('text/html') &&
/* eslint-disable quotes, comma-spacing */
isPathWhitelisted([], event.request.url)) {
/* eslint-enable quotes, comma-spacing */
var navigateFallbackUrl = new URL(navigateFallback, self.location);
cacheName = AbsoluteUrlToCacheName[navigateFallbackUrl.toString()];
}
if (cacheName) {
event.respondWith(
// Rely on the fact that each cache we manage should only have one entry, and return that.
caches.open(cacheName).then(function(cache) {
return cache.keys().then(function(keys) {
return cache.match(keys[0]).then(function(response) {
if (response) {
return response;
}
// If for some reason the response was deleted from the cache,
// raise and exception and fall back to the fetch() triggered in the catch().
throw Error('The cache ' + cacheName + ' is empty.');
});
});
}).catch(function(e) {
console.warn('Couldn\'t serve response for "%s" from cache: %O', event.request.url, e);
return fetch(event.request);
})
);
}
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -11,7 +11,7 @@ from itertools import groupby
from homeassistant.components import recorder, script
import homeassistant.util.dt as dt_util
from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.components.http import HomeAssistantView
DOMAIN = 'history'
DEPENDENCIES = ['recorder', 'http']
@@ -155,49 +155,44 @@ def get_state(utc_point_in_time, entity_id, run=None):
# pylint: disable=unused-argument
def setup(hass, config):
"""Setup the 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)
hass.wsgi.register_view(Last5StatesView)
hass.wsgi.register_view(HistoryPeriodView)
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')
class Last5StatesView(HomeAssistantView):
"""Handle last 5 state view requests."""
handler.write_json(last_5_states(entity_id))
url = '/api/history/entity/<entity:entity_id>/recent_states'
name = 'api:history:entity-recent-states'
def get(self, request, entity_id):
"""Retrieve last 5 states of entity."""
return self.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)
class HistoryPeriodView(HomeAssistantView):
"""Handle history period requests."""
if date_str:
start_date = dt_util.parse_date(date_str)
url = '/api/history/period'
name = 'api:history:view-period'
extra_urls = ['/api/history/period/<date:date>']
if start_date is None:
handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST)
return
def get(self, request, date=None):
"""Return history over a period of time."""
one_day = timedelta(days=1)
start_time = dt_util.as_utc(dt_util.start_of_local_day(start_date))
else:
start_time = dt_util.utcnow() - one_day
if date:
start_time = dt_util.as_utc(dt_util.start_of_local_day(date))
else:
start_time = dt_util.utcnow() - one_day
end_time = start_time + one_day
end_time = start_time + one_day
entity_id = request.args.get('filter_entity_id')
entity_id = data.get('filter_entity_id')
handler.write_json(
get_significant_states(start_time, end_time, entity_id).values())
return self.json(
get_significant_states(start_time, end_time, entity_id).values())
def _is_significant(state):

View File

@@ -1,41 +1,25 @@
"""
This module provides an API and a HTTP interface for debug purposes.
For more details about the RESTful API, please refer to the documentation at
https://home-assistant.io/developers/api/
"""
import gzip
"""This module provides WSGI application to serve the Home Assistant API."""
import hmac
import json
import logging
import ssl
import mimetypes
import threading
import time
from datetime import timedelta
from http import cookies
from http.server import HTTPServer, SimpleHTTPRequestHandler
from socketserver import ThreadingMixIn
from urllib.parse import parse_qs, urlparse
import re
import voluptuous as vol
import homeassistant.bootstrap as bootstrap
import homeassistant.core as ha
import homeassistant.remote as rem
import homeassistant.util as util
import homeassistant.util.dt as date_util
import homeassistant.helpers.config_validation as cv
from homeassistant import util
from homeassistant.const import (
CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN, HTTP_HEADER_ACCEPT_ENCODING,
HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_CONTENT_ENCODING,
HTTP_HEADER_CONTENT_LENGTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_EXPIRES,
HTTP_HEADER_HA_AUTH, HTTP_HEADER_VARY,
SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, HTTP_METHOD_NOT_ALLOWED,
HTTP_NOT_FOUND, HTTP_OK, HTTP_UNAUTHORIZED, HTTP_UNPROCESSABLE_ENTITY,
ALLOWED_CORS_HEADERS,
SERVER_PORT, URL_ROOT, URL_API_EVENT_FORWARD)
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS)
from homeassistant.helpers.entity import split_entity_id
import homeassistant.util.dt as dt_util
import homeassistant.helpers.config_validation as cv
DOMAIN = "http"
REQUIREMENTS = ("eventlet==0.19.0", "static3==0.7.0", "Werkzeug==0.11.5",)
CONF_API_PASSWORD = "api_password"
CONF_SERVER_HOST = "server_host"
@@ -47,10 +31,7 @@ CONF_CORS_ORIGINS = 'cors_allowed_origins'
DATA_API_PASSWORD = 'api_password'
# Throttling time in seconds for expired sessions check
SESSION_CLEAR_INTERVAL = timedelta(seconds=20)
SESSION_TIMEOUT_SECONDS = 1800
SESSION_KEY = 'sessionId'
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
_LOGGER = logging.getLogger(__name__)
@@ -68,13 +49,32 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
class HideSensitiveFilter(logging.Filter):
"""Filter API password calls."""
# pylint: disable=too-few-public-methods
def __init__(self, hass):
"""Initialize sensitive data filter."""
super().__init__()
self.hass = hass
def filter(self, record):
"""Hide sensitive data in messages."""
if self.hass.wsgi.api_password is None:
return True
record.msg = record.msg.replace(self.hass.wsgi.api_password, '*******')
return True
def setup(hass, config):
"""Set up the HTTP API and debug interface."""
_LOGGER.addFilter(HideSensitiveFilter(hass))
conf = config.get(DOMAIN, {})
api_password = util.convert(conf.get(CONF_API_PASSWORD), str)
# If no server host is given, accept all incoming requests
server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0')
server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT)
development = str(conf.get(CONF_DEVELOPMENT, "")) == "1"
@@ -82,22 +82,24 @@ def setup(hass, config):
ssl_key = conf.get(CONF_SSL_KEY)
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
try:
server = HomeAssistantHTTPServer(
(server_host, server_port), RequestHandler, hass, api_password,
development, ssl_certificate, ssl_key, cors_origins)
except OSError:
# If address already in use
_LOGGER.exception("Error setting up HTTP server")
return False
server = HomeAssistantWSGI(
hass,
development=development,
server_host=server_host,
server_port=server_port,
api_password=api_password,
ssl_certificate=ssl_certificate,
ssl_key=ssl_key,
cors_origins=cors_origins
)
hass.bus.listen_once(
ha.EVENT_HOMEASSISTANT_START,
lambda event:
threading.Thread(target=server.start, daemon=True,
name='HTTP-server').start())
name='WSGI-server').start())
hass.http = server
hass.wsgi = server
hass.config.api = rem.API(server_host if server_host != '0.0.0.0'
else util.get_local_ip(),
api_password, server_port,
@@ -106,413 +108,338 @@ def setup(hass, config):
return True
# pylint: disable=too-many-instance-attributes
class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
"""Handle HTTP requests in a threaded fashion."""
def request_class():
"""Generate request class.
# pylint: disable=too-few-public-methods
allow_reuse_address = True
daemon_threads = True
Done in method because of imports.
"""
from werkzeug.exceptions import BadRequest
from werkzeug.wrappers import BaseRequest, AcceptMixin
from werkzeug.utils import cached_property
class Request(BaseRequest, AcceptMixin):
"""Base class for incoming requests."""
@cached_property
def json(self):
"""Get the result of json.loads if possible."""
if not self.data:
return None
# elif 'json' not in self.environ.get('CONTENT_TYPE', ''):
# raise BadRequest('Not a JSON request')
try:
return json.loads(self.data.decode(
self.charset, self.encoding_errors))
except (TypeError, ValueError):
raise BadRequest('Unable to read JSON request')
return Request
def routing_map(hass):
"""Generate empty routing map with HA validators."""
from werkzeug.routing import Map, BaseConverter, ValidationError
class EntityValidator(BaseConverter):
"""Validate entity_id in urls."""
regex = r"(\w+)\.(\w+)"
def __init__(self, url_map, exist=True, domain=None):
"""Initilalize entity validator."""
super().__init__(url_map)
self._exist = exist
self._domain = domain
def to_python(self, value):
"""Validate entity id."""
if self._exist and hass.states.get(value) is None:
raise ValidationError()
if self._domain is not None and \
split_entity_id(value)[0] != self._domain:
raise ValidationError()
return value
def to_url(self, value):
"""Convert entity_id for a url."""
return value
class DateValidator(BaseConverter):
"""Validate dates in urls."""
regex = r'\d{4}-\d{1,2}-\d{1,2}'
def to_python(self, value):
"""Validate and convert date."""
parsed = dt_util.parse_date(value)
if parsed is None:
raise ValidationError()
return parsed
def to_url(self, value):
"""Convert date to url value."""
return value.isoformat()
return Map(converters={
'entity': EntityValidator,
'date': DateValidator,
})
class HomeAssistantWSGI(object):
"""WSGI server for Home Assistant."""
# pylint: disable=too-many-instance-attributes, too-many-locals
# pylint: disable=too-many-arguments
def __init__(self, server_address, request_handler_class,
hass, api_password, development, ssl_certificate, ssl_key,
cors_origins):
"""Initialize the server."""
super().__init__(server_address, request_handler_class)
self.server_address = server_address
def __init__(self, hass, development, api_password, ssl_certificate,
ssl_key, server_host, server_port, cors_origins):
"""Initilalize the WSGI Home Assistant server."""
from werkzeug.wrappers import Response
Response.mimetype = 'text/html'
# pylint: disable=invalid-name
self.Request = request_class()
self.url_map = routing_map(hass)
self.views = {}
self.hass = hass
self.api_password = api_password
self.extra_apps = {}
self.development = development
self.paths = []
self.sessions = SessionStore()
self.use_ssl = ssl_certificate is not None
self.api_password = api_password
self.ssl_certificate = ssl_certificate
self.ssl_key = ssl_key
self.server_host = server_host
self.server_port = server_port
self.cors_origins = cors_origins
# We will lazy init this one if needed
self.event_forwarder = None
if development:
_LOGGER.info("running http in development mode")
def register_view(self, view):
"""Register a view with the WSGI server.
if ssl_certificate is not None:
context = ssl.create_default_context(
purpose=ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(ssl_certificate, keyfile=ssl_key)
self.socket = context.wrap_socket(self.socket, server_side=True)
The view argument must be a class that inherits from HomeAssistantView.
It is optional to instantiate it before registering; this method will
handle it either way.
"""
from werkzeug.routing import Rule
if view.name in self.views:
_LOGGER.warning("View '%s' is being overwritten", view.name)
if isinstance(view, type):
# Instantiate the view, if needed
view = view(self.hass)
self.views[view.name] = view
rule = Rule(view.url, endpoint=view.name)
self.url_map.add(rule)
for url in view.extra_urls:
rule = Rule(url, endpoint=view.name)
self.url_map.add(rule)
def register_redirect(self, url, redirect_to):
"""Register a redirect with the server.
If given this must be either a string or callable. In case of a
callable it's called with the url adapter that triggered the match and
the values of the URL as keyword arguments and has to return the target
for the redirect, otherwise it has to be a string with placeholders in
rule syntax.
"""
from werkzeug.routing import Rule
self.url_map.add(Rule(url, redirect_to=redirect_to))
def register_static_path(self, url_root, path, cache_length=31):
"""Register a folder to serve as a static path.
Specify optional cache length of asset in days.
"""
from static import Cling
headers = []
if cache_length and not self.development:
# 1 year in seconds
cache_time = cache_length * 86400
headers.append({
'prefix': '',
HTTP_HEADER_CACHE_CONTROL:
"public, max-age={}".format(cache_time)
})
self.register_wsgi_app(url_root, Cling(path, headers=headers))
def register_wsgi_app(self, url_root, app):
"""Register a path to serve a WSGI app."""
if url_root in self.extra_apps:
_LOGGER.warning("Url root '%s' is being overwritten", url_root)
self.extra_apps[url_root] = app
def start(self):
"""Start the HTTP server."""
def stop_http(event):
"""Stop the HTTP server."""
self.shutdown()
"""Start the wsgi server."""
from eventlet import wsgi
import eventlet
self.hass.bus.listen_once(ha.EVENT_HOMEASSISTANT_STOP, stop_http)
sock = eventlet.listen((self.server_host, self.server_port))
if self.ssl_certificate:
sock = eventlet.wrap_ssl(sock, certfile=self.ssl_certificate,
keyfile=self.ssl_key, server_side=True)
wsgi.server(sock, self, log=_LOGGER)
protocol = 'https' if self.use_ssl else 'http'
def dispatch_request(self, request):
"""Handle incoming request."""
from werkzeug.exceptions import (
MethodNotAllowed, NotFound, BadRequest, Unauthorized,
)
from werkzeug.routing import RequestRedirect
_LOGGER.info(
"Starting web interface at %s://%s:%d",
protocol, self.server_address[0], self.server_address[1])
with request:
adapter = self.url_map.bind_to_environ(request.environ)
try:
endpoint, values = adapter.match()
return self.views[endpoint].handle_request(request, **values)
except RequestRedirect as ex:
return ex
except (BadRequest, NotFound, MethodNotAllowed,
Unauthorized) as ex:
resp = ex.get_response(request.environ)
if request.accept_mimetypes.accept_json:
resp.data = json.dumps({
"result": "error",
"message": str(ex),
})
resp.mimetype = "application/json"
return resp
# 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')
def base_app(self, environ, start_response):
"""WSGI Handler of requests to base app."""
request = self.Request(environ)
response = self.dispatch_request(request)
self.serve_forever()
if self.cors_origins:
cors_check = (environ.get("HTTP_ORIGIN") in self.cors_origins)
cors_headers = ", ".join(ALLOWED_CORS_HEADERS)
if cors_check:
response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN] = \
environ.get("HTTP_ORIGIN")
response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS] = \
cors_headers
def register_path(self, method, url, callback, require_auth=True):
"""Register a path with the server."""
self.paths.append((method, url, callback, require_auth))
return response(environ, start_response)
def log_message(self, fmt, *args):
"""Redirect built-in log to HA logging."""
# pylint: disable=no-self-use
_LOGGER.info(fmt, *args)
def __call__(self, environ, start_response):
"""Handle a request for base app + extra apps."""
from werkzeug.wsgi import DispatcherMiddleware
app = DispatcherMiddleware(self.base_app, self.extra_apps)
# Strip out any cachebusting MD5 fingerprints
fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', ''))
if fingerprinted:
environ['PATH_INFO'] = "{}.{}".format(*fingerprinted.groups())
return app(environ, start_response)
# pylint: disable=too-many-public-methods,too-many-locals
class RequestHandler(SimpleHTTPRequestHandler):
"""Handle incoming HTTP requests.
class HomeAssistantView(object):
"""Base view for all views."""
We extend from SimpleHTTPRequestHandler instead of Base so we
can use the guess content type methods.
"""
extra_urls = []
requires_auth = True # Views inheriting from this class can override this
server_version = "HomeAssistant/1.0"
def __init__(self, hass):
"""Initilalize the base view."""
from werkzeug.wrappers import Response
def __init__(self, req, client_addr, server):
"""Constructor, call the base constructor and set up session."""
# Track if this was an authenticated request
self.authenticated = False
SimpleHTTPRequestHandler.__init__(self, req, client_addr, server)
self.protocol_version = 'HTTP/1.1'
if not hasattr(self, 'url'):
class_name = self.__class__.__name__
raise AttributeError(
'{0} missing required attribute "url"'.format(class_name)
)
def log_message(self, fmt, *arguments):
"""Redirect built-in log to HA logging."""
if self.server.api_password is None:
_LOGGER.info(fmt, *arguments)
else:
_LOGGER.info(
fmt, *(arg.replace(self.server.api_password, '*******')
if isinstance(arg, str) else arg for arg in arguments))
if not hasattr(self, 'name'):
class_name = self.__class__.__name__
raise AttributeError(
'{0} missing required attribute "name"'.format(class_name)
)
def _handle_request(self, method): # pylint: disable=too-many-branches
"""Perform some common checks and call appropriate method."""
url = urlparse(self.path)
self.hass = hass
# pylint: disable=invalid-name
self.Response = Response
# Read query input. parse_qs gives a list for each value, we want last
data = {key: data[-1] for key, data in parse_qs(url.query).items()}
def handle_request(self, request, **values):
"""Handle request to url."""
from werkzeug.exceptions import MethodNotAllowed, Unauthorized
# Did we get post input ?
content_length = int(self.headers.get(HTTP_HEADER_CONTENT_LENGTH, 0))
try:
handler = getattr(self, request.method.lower())
except AttributeError:
raise MethodNotAllowed
if content_length:
body_content = self.rfile.read(content_length).decode("UTF-8")
# Auth code verbose on purpose
authenticated = False
if self.hass.wsgi.api_password is None:
authenticated = True
elif hmac.compare_digest(request.headers.get(HTTP_HEADER_HA_AUTH, ''),
self.hass.wsgi.api_password):
# A valid auth header has been set
authenticated = True
elif hmac.compare_digest(request.args.get(DATA_API_PASSWORD, ''),
self.hass.wsgi.api_password):
authenticated = True
if self.requires_auth and not authenticated:
raise Unauthorized()
request.authenticated = authenticated
result = handler(request, **values)
if isinstance(result, self.Response):
# The method handler returned a ready-made Response, how nice of it
return result
status_code = 200
if isinstance(result, tuple):
result, status_code = result
return self.Response(result, status=status_code)
def json(self, result, status_code=200):
"""Return a JSON response."""
msg = json.dumps(
result,
sort_keys=True,
cls=rem.JSONEncoder
).encode('UTF-8')
return self.Response(msg, mimetype="application/json",
status=status_code)
def json_message(self, error, status_code=200):
"""Return a JSON message response."""
return self.json({'message': error}, status_code)
def file(self, request, fil, mimetype=None):
"""Return a file."""
from werkzeug.wsgi import wrap_file
from werkzeug.exceptions import NotFound
if isinstance(fil, str):
if mimetype is None:
mimetype = mimetypes.guess_type(fil)[0]
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
fil = open(fil)
except IOError:
raise NotFound()
if self.verify_session():
# The user has a valid session already
self.authenticated = True
elif self.server.api_password is None:
# No password is set, so everyone is authenticated
self.authenticated = True
elif hmac.compare_digest(self.headers.get(HTTP_HEADER_HA_AUTH, ''),
self.server.api_password):
# A valid auth header has been set
self.authenticated = True
elif hmac.compare_digest(data.get(DATA_API_PASSWORD, ''),
self.server.api_password):
# A valid password has been specified
self.authenticated = True
else:
self.authenticated = False
# we really shouldn't need to forward the password from here
if url.path not in [URL_ROOT, URL_API_EVENT_FORWARD]:
data.pop(DATA_API_PASSWORD, None)
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
msg = "API password missing or incorrect."
if require_auth and not self.authenticated:
self.write_json_message(msg, HTTP_UNAUTHORIZED)
_LOGGER.warning('%s Source IP: %s',
msg,
self.client_address[0])
return
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."""
json_data = json.dumps(data, indent=4, sort_keys=True,
cls=rem.JSONEncoder).encode('UTF-8')
self.send_response(status_code)
if location:
self.send_header('Location', location)
self.set_session_cookie_header()
self.write_content(json_data, CONTENT_TYPE_JSON)
def write_text(self, message, status_code=HTTP_OK):
"""Helper method to return a text message to the caller."""
msg_data = message.encode('UTF-8')
self.send_response(status_code)
self.set_session_cookie_header()
self.write_content(msg_data, CONTENT_TYPE_TEXT_PLAIN)
def write_file(self, path, cache_headers=True):
"""Return a file to the user."""
try:
with open(path, 'rb') as inp:
self.write_file_pointer(self.guess_type(path), inp,
cache_headers)
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, cache_headers=True):
"""Helper function to write a file pointer to the user."""
self.send_response(HTTP_OK)
if cache_headers:
self.set_cache_header()
self.set_session_cookie_header()
self.write_content(inp.read(), content_type)
def write_content(self, content, content_type=None):
"""Helper method to write content bytes to output stream."""
if content_type is not None:
self.send_header(HTTP_HEADER_CONTENT_TYPE, content_type)
if 'gzip' in self.headers.get(HTTP_HEADER_ACCEPT_ENCODING, ''):
content = gzip.compress(content)
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(content)))
cors_check = (self.headers.get("Origin") in self.server.cors_origins)
cors_headers = ", ".join(ALLOWED_CORS_HEADERS)
if self.server.cors_origins and cors_check:
self.send_header(HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
self.headers.get("Origin"))
self.send_header(HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
cors_headers)
self.end_headers()
if self.command == 'HEAD':
return
self.wfile.write(content)
def set_cache_header(self):
"""Add cache headers if not in development."""
if self.server.development:
return
# 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 and return session ID."""
if not self.authenticated:
return None
session_id = self.get_cookie_session_id()
if session_id is not None:
self.server.sessions.extend_validation(session_id)
return session_id
self.send_header(
'Set-Cookie',
'{}={}'.format(SESSION_KEY, self.server.sessions.create())
)
return session_id
def verify_session(self):
"""Verify that we are in a valid session."""
return self.get_cookie_session_id() is not None
def get_cookie_session_id(self):
"""Extract the current session ID from the cookie.
Return None if not set or invalid.
"""
if 'Cookie' not in self.headers:
return None
cookie = cookies.SimpleCookie()
try:
cookie.load(self.headers["Cookie"])
except cookies.CookieError:
return None
morsel = cookie.get(SESSION_KEY)
if morsel is None:
return None
session_id = cookie[SESSION_KEY].value
if self.server.sessions.is_valid(session_id):
return session_id
return None
def destroy_session(self):
"""Destroy the session."""
session_id = self.get_cookie_session_id()
if session_id is None:
return
self.send_header('Set-Cookie', '')
self.server.sessions.destroy(session_id)
def session_valid_time():
"""Time till when a session will be valid."""
return date_util.utcnow() + timedelta(seconds=SESSION_TIMEOUT_SECONDS)
class SessionStore(object):
"""Responsible for storing and retrieving HTTP sessions."""
def __init__(self):
"""Setup the session store."""
self._sessions = {}
self._lock = threading.RLock()
@util.Throttle(SESSION_CLEAR_INTERVAL)
def _remove_expired(self):
"""Remove any expired sessions."""
now = date_util.utcnow()
for key in [key for key, valid_time in self._sessions.items()
if valid_time < now]:
self._sessions.pop(key)
def is_valid(self, key):
"""Return True if a valid session is given."""
with self._lock:
self._remove_expired()
return (key in self._sessions and
self._sessions[key] > date_util.utcnow())
def extend_validation(self, key):
"""Extend a session validation time."""
with self._lock:
if key not in self._sessions:
return
self._sessions[key] = session_valid_time()
def destroy(self, key):
"""Destroy a session by key."""
with self._lock:
self._sessions.pop(key, None)
def create(self):
"""Create a new session."""
with self._lock:
session_id = util.get_random_string(20)
while session_id in self._sessions:
session_id = util.get_random_string(20)
self._sessions[session_id] = session_valid_time()
return session_id
return self.Response(wrap_file(request.environ, fil),
mimetype=mimetype, direct_passthrough=True)

View File

@@ -29,7 +29,7 @@ SERVICE_SET_AUX_HEAT = "set_aux_heat"
SERVICE_SET_TEMPERATURE = "set_temperature"
SERVICE_SET_FAN_MODE = "set_fan_mode"
SERVICE_SET_OPERATION_MODE = "set_operation_mode"
SERVICE_SET_SWING = "set_swing_mode"
SERVICE_SET_SWING_MODE = "set_swing_mode"
SERVICE_SET_HUMIDITY = "set_humidity"
STATE_HEAT = "heat"
@@ -40,17 +40,17 @@ STATE_DRY = "dry"
STATE_FAN_ONLY = "fan_only"
ATTR_CURRENT_TEMPERATURE = "current_temperature"
ATTR_CURRENT_HUMIDITY = "current_humidity"
ATTR_HUMIDITY = "humidity"
ATTR_AWAY_MODE = "away_mode"
ATTR_AUX_HEAT = "aux_heat"
ATTR_FAN = "fan"
ATTR_FAN_LIST = "fan_list"
ATTR_MAX_TEMP = "max_temp"
ATTR_MIN_TEMP = "min_temp"
ATTR_AWAY_MODE = "away_mode"
ATTR_AUX_HEAT = "aux_heat"
ATTR_FAN_MODE = "fan_mode"
ATTR_FAN_LIST = "fan_list"
ATTR_CURRENT_HUMIDITY = "current_humidity"
ATTR_HUMIDITY = "humidity"
ATTR_MAX_HUMIDITY = "max_humidity"
ATTR_MIN_HUMIDITY = "min_humidity"
ATTR_OPERATION = "operation_mode"
ATTR_OPERATION_MODE = "operation_mode"
ATTR_OPERATION_LIST = "operation_list"
ATTR_SWING_MODE = "swing_mode"
ATTR_SWING_LIST = "swing_list"
@@ -108,7 +108,7 @@ def set_humidity(hass, humidity, entity_id=None):
def set_fan_mode(hass, fan, entity_id=None):
"""Turn all or specified hvac fan mode on."""
data = {ATTR_FAN: fan}
data = {ATTR_FAN_MODE: fan}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
@@ -118,7 +118,7 @@ def set_fan_mode(hass, fan, entity_id=None):
def set_operation_mode(hass, operation_mode, entity_id=None):
"""Set new target operation mode."""
data = {ATTR_OPERATION: operation_mode}
data = {ATTR_OPERATION_MODE: operation_mode}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
@@ -133,7 +133,7 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_SWING, data)
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
# pylint: disable=too-many-branches
@@ -247,12 +247,12 @@ def setup(hass, config):
"""Set fan mode on target hvacs."""
target_hvacs = component.extract_from_service(service)
fan = service.data.get(ATTR_FAN)
fan = service.data.get(ATTR_FAN_MODE)
if fan is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_FAN_MODE, ATTR_FAN)
SERVICE_SET_FAN_MODE, ATTR_FAN_MODE)
return
for hvac in target_hvacs:
@@ -269,16 +269,16 @@ def setup(hass, config):
"""Set operating mode on the target hvacs."""
target_hvacs = component.extract_from_service(service)
operation_mode = service.data.get(ATTR_OPERATION)
operation_mode = service.data.get(ATTR_OPERATION_MODE)
if operation_mode is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_OPERATION_MODE, ATTR_OPERATION)
SERVICE_SET_OPERATION_MODE, ATTR_OPERATION_MODE)
return
for hvac in target_hvacs:
hvac.set_operation(operation_mode)
hvac.set_operation_mode(operation_mode)
if hvac.should_poll:
hvac.update_ha_state(True)
@@ -296,18 +296,18 @@ def setup(hass, config):
if swing_mode is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_SWING, ATTR_SWING_MODE)
SERVICE_SET_SWING_MODE, ATTR_SWING_MODE)
return
for hvac in target_hvacs:
hvac.set_swing(swing_mode)
hvac.set_swing_mode(swing_mode)
if hvac.should_poll:
hvac.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_SWING, swing_set_service,
descriptions.get(SERVICE_SET_SWING))
DOMAIN, SERVICE_SET_SWING_MODE, swing_set_service,
descriptions.get(SERVICE_SET_SWING_MODE))
return True
@@ -330,19 +330,30 @@ class HvacDevice(Entity):
ATTR_MAX_TEMP: self._convert_for_display(self.max_temp),
ATTR_TEMPERATURE:
self._convert_for_display(self.target_temperature),
ATTR_HUMIDITY: self.target_humidity,
ATTR_CURRENT_HUMIDITY: self.current_humidity,
ATTR_MIN_HUMIDITY: self.min_humidity,
ATTR_MAX_HUMIDITY: self.max_humidity,
ATTR_FAN_LIST: self.fan_list,
ATTR_OPERATION_LIST: self.operation_list,
ATTR_SWING_LIST: self.swing_list,
ATTR_OPERATION: self.current_operation,
ATTR_FAN: self.current_fan_mode,
ATTR_SWING_MODE: self.current_swing_mode,
}
humidity = self.target_humidity
if humidity is not None:
data[ATTR_HUMIDITY] = humidity
data[ATTR_CURRENT_HUMIDITY] = self.current_humidity
data[ATTR_MIN_HUMIDITY] = self.min_humidity
data[ATTR_MAX_HUMIDITY] = self.max_humidity
fan_mode = self.current_fan_mode
if fan_mode is not None:
data[ATTR_FAN_MODE] = fan_mode
data[ATTR_FAN_LIST] = self.fan_list
operation_mode = self.current_operation
if operation_mode is not None:
data[ATTR_OPERATION_MODE] = operation_mode
data[ATTR_OPERATION_LIST] = self.operation_list
swing_mode = self.current_swing_mode
if swing_mode is not None:
data[ATTR_SWING_MODE] = swing_mode
data[ATTR_SWING_LIST] = self.swing_list
is_away = self.is_away_mode_on
if is_away is not None:
data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF
@@ -430,11 +441,11 @@ class HvacDevice(Entity):
"""Set new target fan mode."""
pass
def set_operation(self, operation_mode):
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
pass
def set_swing(self, swing_mode):
def set_swing_mode(self, swing_mode):
"""Set new target swing operation."""
pass
@@ -457,12 +468,12 @@ class HvacDevice(Entity):
@property
def min_temp(self):
"""Return the minimum temperature."""
return self._convert_for_display(7)
return convert(19, TEMP_CELCIUS, self.unit_of_measurement)
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._convert_for_display(35)
return convert(30, TEMP_CELCIUS, self.unit_of_measurement)
@property
def min_humidity(self):

View File

@@ -118,7 +118,7 @@ class DemoHvac(HvacDevice):
self._target_humidity = humidity
self.update_ha_state()
def set_swing(self, swing_mode):
def set_swing_mode(self, swing_mode):
"""Set new target temperature."""
self._current_swing_mode = swing_mode
self.update_ha_state()
@@ -128,7 +128,7 @@ class DemoHvac(HvacDevice):
self._current_fan_mode = fan
self.update_ha_state()
def set_operation(self, operation_mode):
def set_operation_mode(self, operation_mode):
"""Set new target temperature."""
self._current_operation = operation_mode
self.update_ha_state()

55
homeassistant/components/hvac/zwave.py Normal file → Executable file
View File

@@ -1,5 +1,9 @@
"""ZWave Hvac device."""
"""
Support for ZWave HVAC devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/hvac.zwave/
"""
# Because we do not compile openzwave on CI
# pylint: disable=import-error
import logging
@@ -19,6 +23,12 @@ REMOTEC = 0x5254
REMOTEC_ZXT_120 = 0x8377
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120, 0)
COMMAND_CLASS_SENSOR_MULTILEVEL = 0x31
COMMAND_CLASS_THERMOSTAT_MODE = 0x40
COMMAND_CLASS_THERMOSTAT_SETPOINT = 0x43
COMMAND_CLASS_THERMOSTAT_FAN_MODE = 0x44
COMMAND_CLASS_CONFIGURATION = 0x70
WORKAROUND_ZXT_120 = 'zxt_120'
DEVICE_MAPPINGS = {
@@ -96,22 +106,24 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
def update_properties(self):
"""Callback on data change for the registered node/value pair."""
# Set point
for value in self._node.get_values(class_id=0x43).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
if int(value.data) != 0:
self._target_temperature = int(value.data)
# Operation Mode
for value in self._node.get_values(class_id=0x40).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
self._current_operation = value.data
self._operation_list = list(value.data_items)
_LOGGER.debug("self._operation_list=%s", self._operation_list)
# Current Temp
for value in self._node.get_values(class_id=0x31).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_SENSOR_MULTILEVEL).values():
self._current_temperature = int(value.data)
self._unit = value.units
# Fan Mode
fan_class_id = 0x44 if self._zxt_120 else 0x42
_LOGGER.debug("fan_class_id=%s", fan_class_id)
for value in self._node.get_values(class_id=fan_class_id).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
self._current_operation_state = value.data
self._fan_list = list(value.data_items)
_LOGGER.debug("self._fan_list=%s", self._fan_list)
@@ -119,7 +131,8 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
self._current_operation_state)
# Swing mode
if self._zxt_120 == 1:
for value in self._node.get_values(class_id=0x70).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_CONFIGURATION).values():
if value.command_class == 112 and value.index == 33:
self._current_swing_mode = value.data
self._swing_list = [0, 1]
@@ -184,7 +197,8 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
def set_temperature(self, temperature):
"""Set new target temperature."""
for value in self._node.get_values(class_id=0x43).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
if value.command_class != 67:
continue
if self._zxt_120:
@@ -200,29 +214,22 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
def set_fan_mode(self, fan):
"""Set new target fan mode."""
for value in self._node.get_values(class_id=0x44).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
if value.command_class == 68 and value.index == 0:
value.data = bytes(fan, 'utf-8')
def set_operation(self, operation_mode):
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
for value in self._node.get_values(class_id=0x40).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
if value.command_class == 64 and value.index == 0:
value.data = bytes(operation_mode, 'utf-8')
def set_swing(self, swing_mode):
def set_swing_mode(self, swing_mode):
"""Set new target swing mode."""
if self._zxt_120 == 1:
for value in self._node.get_values(class_id=0x70).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_CONFIGURATION).values():
if value.command_class == 112 and value.index == 33:
value.data = int(swing_mode)
@property
def min_temp(self):
"""Return the minimum temperature."""
return self._convert_for_display(19)
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._convert_for_display(30)

View File

@@ -11,7 +11,6 @@ from homeassistant.const import (
ATTR_DISCOVERED, ATTR_SERVICE, CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME,
EVENT_PLATFORM_DISCOVERED)
from homeassistant.helpers import validate_config
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.loader import get_component
DOMAIN = "insteon_hub"
@@ -53,43 +52,3 @@ def setup(hass, config):
EVENT_PLATFORM_DISCOVERED,
{ATTR_SERVICE: discovery, ATTR_DISCOVERED: {}})
return True
class InsteonToggleDevice(ToggleEntity):
"""An abstract Class for an Insteon node."""
def __init__(self, node):
"""Initialize the device."""
self.node = node
self._value = 0
@property
def name(self):
"""Return the the name of the node."""
return self.node.DeviceName
@property
def unique_id(self):
"""Return the ID of this insteon node."""
return self.node.DeviceID
def update(self):
"""Update state of the sensor."""
resp = self.node.send_command('get_status', wait=True)
try:
self._value = resp['response']['level']
except KeyError:
pass
@property
def is_on(self):
"""Return the boolean response if the node is on."""
return self._value != 0
def turn_on(self, **kwargs):
"""Turn device on."""
self.node.send_command('on')
def turn_off(self, **kwargs):
"""Turn device off."""
self.node.send_command('off')

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers.entity import ToggleEntity
from homeassistant.loader import get_component
DOMAIN = "isy994"
REQUIREMENTS = ['PyISY==1.0.5']
REQUIREMENTS = ['PyISY==1.0.6']
DISCOVER_LIGHTS = "isy994.lights"
DISCOVER_SWITCHES = "isy994.switches"
DISCOVER_SENSORS = "isy994.sensors"

View File

@@ -39,6 +39,7 @@ ATTR_TRANSITION = "transition"
ATTR_RGB_COLOR = "rgb_color"
ATTR_XY_COLOR = "xy_color"
ATTR_COLOR_TEMP = "color_temp"
ATTR_COLOR_NAME = "color_name"
# int with value 0 .. 255 representing brightness of the light.
ATTR_BRIGHTNESS = "brightness"
@@ -87,6 +88,7 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({
ATTR_PROFILE: str,
ATTR_TRANSITION: VALID_TRANSITION,
ATTR_BRIGHTNESS: cv.byte,
ATTR_COLOR_NAME: str,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
@@ -122,7 +124,7 @@ def is_on(hass, entity_id=None):
# pylint: disable=too-many-arguments
def turn_on(hass, entity_id=None, transition=None, brightness=None,
rgb_color=None, xy_color=None, color_temp=None, profile=None,
flash=None, effect=None):
flash=None, effect=None, color_name=None):
"""Turn all or specified light on."""
data = {
key: value for key, value in [
@@ -135,6 +137,7 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None,
(ATTR_COLOR_TEMP, color_temp),
(ATTR_FLASH, flash),
(ATTR_EFFECT, effect),
(ATTR_COLOR_NAME, color_name),
] if value is not None
}
@@ -228,6 +231,11 @@ def setup(hass, config):
params.setdefault(ATTR_XY_COLOR, profile[:2])
params.setdefault(ATTR_BRIGHTNESS, profile[2])
color_name = params.pop(ATTR_COLOR_NAME, None)
if color_name is not None:
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
for light in target_lights:
light.turn_on(**params)

View File

@@ -0,0 +1,92 @@
"""
Support for EnOcean light sources.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.enocean/
"""
import logging
import math
from homeassistant.components.light import Light, ATTR_BRIGHTNESS
from homeassistant.const import CONF_NAME
from homeassistant.components import enocean
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["enocean"]
CONF_ID = "id"
CONF_SENDER_ID = "sender_id"
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the EnOcean light platform."""
sender_id = config.get(CONF_SENDER_ID, None)
devname = config.get(CONF_NAME, "Enocean actuator")
dev_id = config.get(CONF_ID, [0x00, 0x00, 0x00, 0x00])
add_devices([EnOceanLight(sender_id, devname, dev_id)])
class EnOceanLight(enocean.EnOceanDevice, Light):
"""Representation of an EnOcean light source."""
def __init__(self, sender_id, devname, dev_id):
"""Initialize the EnOcean light source."""
enocean.EnOceanDevice.__init__(self)
self._on_state = False
self._brightness = 50
self._sender_id = sender_id
self.dev_id = dev_id
self._devname = devname
self.stype = "dimmer"
@property
def name(self):
"""Return the name of the device if any."""
return self._devname
@property
def brightness(self):
"""Brightness of the light.
This method is optional. Removing it indicates to Home Assistant
that brightness is not supported for this light.
"""
return self._brightness
@property
def is_on(self):
"""If light is on."""
return self._on_state
def turn_on(self, **kwargs):
"""Turn the light source on or sets a specific dimmer value."""
brightness = kwargs.get(ATTR_BRIGHTNESS)
if brightness is not None:
self._brightness = brightness
bval = math.floor(self._brightness / 256.0 * 100.0)
if bval == 0:
bval = 1
command = [0xa5, 0x02, bval, 0x01, 0x09]
command.extend(self._sender_id)
command.extend([0x00])
self.send_command(command, [], 0x01)
self._on_state = True
def turn_off(self, **kwargs):
"""Turn the light source off."""
command = [0xa5, 0x02, 0x00, 0x01, 0x09]
command.extend(self._sender_id)
command.extend([0x00])
self.send_command(command, [], 0x01)
self._on_state = False
def value_changed(self, val):
"""Update the internal state of this device in HA."""
self._brightness = math.floor(val / 100.0 * 256.0)
self._on_state = bool(val != 0)
self.update_ha_state()

View File

@@ -235,14 +235,16 @@ class HueLight(Light):
if ATTR_TRANSITION in kwargs:
command['transitiontime'] = 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]
elif ATTR_RGB_COLOR in kwargs:
command['xy'] = color_util.color_RGB_to_xy(
xyb = color_util.color_RGB_to_xy(
*(int(val) for val in kwargs[ATTR_RGB_COLOR]))
command['xy'] = xyb[0], xyb[1]
command['bri'] = xyb[2]
if ATTR_BRIGHTNESS in kwargs:
command['bri'] = kwargs[ATTR_BRIGHTNESS]
if ATTR_COLOR_TEMP in kwargs:
command['ct'] = kwargs[ATTR_COLOR_TEMP]

View File

@@ -4,7 +4,8 @@ Support for Insteon Hub lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/insteon_hub/
"""
from homeassistant.components.insteon_hub import INSTEON, InsteonToggleDevice
from homeassistant.components.insteon_hub import INSTEON
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -16,3 +17,53 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if device.DeviceCategory == "Dimmable Lighting Control":
devs.append(InsteonToggleDevice(device))
add_devices(devs)
class InsteonToggleDevice(Light):
"""An abstract Class for an Insteon node."""
def __init__(self, node):
"""Initialize the device."""
self.node = node
self._value = 0
@property
def name(self):
"""Return the the name of the node."""
return self.node.DeviceName
@property
def unique_id(self):
"""Return the ID of this insteon node."""
return self.node.DeviceID
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._value / 100 * 255
def update(self):
"""Update state of the sensor."""
resp = self.node.send_command('get_status', wait=True)
try:
self._value = resp['response']['level']
except KeyError:
pass
@property
def is_on(self):
"""Return the boolean response if the node is on."""
return self._value != 0
def turn_on(self, **kwargs):
"""Turn device on."""
if ATTR_BRIGHTNESS in kwargs:
self._value = kwargs[ATTR_BRIGHTNESS] / 255 * 100
self.node.send_command('on', self._value)
else:
self._value = 100
self.node.send_command('on')
def turn_off(self, **kwargs):
"""Turn device off."""
self.node.send_command('off')

View File

@@ -0,0 +1,165 @@
"""
Support for Osram Lightify.
Uses: https://github.com/aneumeier/python-lightify for the Osram light
interface.
In order to use the platform just add the following to the configuration.yaml:
light:
platform: osramlightify
host: <hostname_or_ip>
Todo:
Add support for Non RGBW lights.
"""
import logging
import socket
from datetime import timedelta
from homeassistant import util
from homeassistant.const import CONF_HOST
from homeassistant.components.light import (
Light,
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_RGB_COLOR,
ATTR_TRANSITION
)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['lightify==1.0.3']
TEMP_MIN = 2000 # lightify minimum temperature
TEMP_MAX = 6500 # lightify maximum temperature
TEMP_MIN_HASS = 154 # home assistant minimum temperature
TEMP_MAX_HASS = 500 # home assistant maximum temperature
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Find and return lights."""
import lightify
host = config.get(CONF_HOST)
if host:
try:
bridge = lightify.Lightify(host)
except socket.error as err:
msg = 'Error connecting to bridge: {} due to: {}'.format(host,
str(err))
_LOGGER.exception(msg)
return False
setup_bridge(bridge, add_devices_callback)
else:
_LOGGER.error('No host found in configuration')
return False
def setup_bridge(bridge, add_devices_callback):
"""Setup the Lightify bridge."""
lights = {}
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_lights():
"""Update the lights objects with latest info from bridge."""
bridge.update_all_light_status()
new_lights = []
for (light_id, light) in bridge.lights().items():
if light_id not in lights:
osram_light = OsramLightifyLight(light_id, light,
update_lights)
lights[light_id] = osram_light
new_lights.append(osram_light)
else:
lights[light_id].light = light
if new_lights:
add_devices_callback(new_lights)
update_lights()
class OsramLightifyLight(Light):
"""Defines an Osram Lightify Light."""
def __init__(self, light_id, light, update_lights):
"""Initialize the light."""
self._light = light
self._light_id = light_id
self.update_lights = update_lights
@property
def name(self):
"""Return the name of the device if any."""
return self._light.name()
@property
def rgb_color(self):
"""Last RGB color value set."""
return self._light.rgb()
@property
def color_temp(self):
"""Return the color temperature."""
o_temp = self._light.temp()
temperature = int(TEMP_MIN_HASS + (TEMP_MAX_HASS - TEMP_MIN_HASS) *
(o_temp - TEMP_MIN) / (TEMP_MAX - TEMP_MIN))
return temperature
@property
def brightness(self):
"""Brightness of this light between 0..255."""
return int(self._light.lum() * 2.55)
@property
def is_on(self):
"""Update Status to True if device is on."""
self.update_lights()
_LOGGER.debug("is_on light state for light: %s is: %s",
self._light.name(), self._light.on())
return self._light.on()
def turn_on(self, **kwargs):
"""Turn the device on."""
brightness = 100
if self.brightness:
brightness = int(self.brightness / 2.55)
if ATTR_TRANSITION in kwargs:
fade = kwargs[ATTR_TRANSITION] * 10
else:
fade = 0
if ATTR_RGB_COLOR in kwargs:
red, green, blue = kwargs[ATTR_RGB_COLOR]
self._light.set_rgb(red, green, blue, fade)
if ATTR_BRIGHTNESS in kwargs:
brightness = int(kwargs[ATTR_BRIGHTNESS] / 2.55)
if ATTR_COLOR_TEMP in kwargs:
color_t = kwargs[ATTR_COLOR_TEMP]
kelvin = int(((TEMP_MAX - TEMP_MIN) * (color_t - TEMP_MIN_HASS) /
(TEMP_MAX_HASS - TEMP_MIN_HASS)) + TEMP_MIN)
self._light.set_temperature(kelvin, fade)
self._light.set_luminance(brightness, fade)
self.update_ha_state()
def turn_off(self, **kwargs):
"""Turn the device off."""
if ATTR_TRANSITION in kwargs:
fade = kwargs[ATTR_TRANSITION] * 10
else:
fade = 0
self._light.set_luminance(0, fade)
self.update_ha_state()
def update(self):
"""Synchronize state with bridge."""
self.update_lights(no_throttle=True)

View File

@@ -0,0 +1,35 @@
"""
Support for Qwikswitch Relays and Dimmers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.qwikswitch/
"""
import logging
import homeassistant.components.qwikswitch as qwikswitch
from homeassistant.components.light import Light
DEPENDENCIES = ['qwikswitch']
class QSLight(qwikswitch.QSToggleEntity, Light):
"""Light based on a Qwikswitch relay/dimmer module."""
pass
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Store add_devices for the light components."""
if discovery_info is None or 'qsusb_id' not in discovery_info:
logging.getLogger(__name__).error(
'Configure main Qwikswitch component')
return False
qsusb = qwikswitch.QSUSB[discovery_info['qsusb_id']]
for item in qsusb.ha_devices:
if item['type'] not in ['dim', 'rel']:
continue
dev = QSLight(item, qsusb)
add_devices([dev])
qsusb.ha_objects[item['id']] = dev

View File

@@ -16,6 +16,10 @@ turn_on:
description: Color for the light in RGB-format
example: '[255, 100, 100]'
color_name:
description: A human readable color name
example: 'red'
xy_color:
description: Color for the light in XY-format
example: '[0.52, 0.43]'

View File

@@ -105,6 +105,7 @@ class WemoLight(Light):
elif ATTR_RGB_COLOR in kwargs:
xycolor = color_util.color_RGB_to_xy(
*(int(val) for val in kwargs[ATTR_RGB_COLOR]))
kwargs.setdefault(ATTR_BRIGHTNESS, xycolor[2])
else:
xycolor = None

View File

@@ -97,7 +97,9 @@ class WinkLight(Light):
}
if rgb_color:
state_kwargs['color_xy'] = color_util.color_RGB_to_xy(*rgb_color)
xyb = color_util.color_RGB_to_xy(*rgb_color)
state_kwargs['color_xy'] = xyb[0], xyb[1]
state_kwargs['brightness'] = xyb[2]
if color_temp_mired:
state_kwargs['color_kelvin'] = mired_to_kelvin(color_temp_mired)

View File

@@ -0,0 +1,78 @@
"""
LIRC interface to receive signals from a infrared remote control.
This sensor will momentarily set state to various values as defined
in the .lintrc file which can be interpreted in home-assistant to
trigger various actions.
Sending signals to other IR receivers can be accomplished with the
shell_command component and the irsend command for now.
"""
# pylint: disable=import-error
import threading
import time
import logging
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP,
EVENT_HOMEASSISTANT_START)
DOMAIN = "lirc"
REQUIREMENTS = ['python-lirc==1.2.1']
_LOGGER = logging.getLogger(__name__)
ICON = 'mdi:remote'
EVENT_IR_COMMAND_RECEIVED = 'ir_command_received'
BUTTON_NAME = 'button_name'
def setup(hass, config):
"""Setup LIRC capability."""
import lirc
# blocking=True gives unexpected behavior (multiple responses for 1 press)
# also by not blocking, we allow hass to shut down the thread gracefully
# on exit.
lirc.init('home-assistant', blocking=False)
lirc_interface = LircInterface(hass)
def _start_lirc(_event):
lirc_interface.start()
def _stop_lirc(_event):
lirc_interface.stopped.set()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_lirc)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_lirc)
return True
class LircInterface(threading.Thread):
"""
This interfaces with the lirc daemon to read IR commands.
When using lirc in blocking mode, sometimes repeated commands get produced
in the next read of a command so we use a thread here to just wait
around until a non-empty response is obtained from lirc.
"""
def __init__(self, hass):
"""Construct a LIRC interface object."""
threading.Thread.__init__(self)
self.daemon = True
self.stopped = threading.Event()
self.hass = hass
def run(self):
"""Main loop of LIRC interface thread."""
import lirc
while not self.stopped.isSet():
code = lirc.nextcode() # list; empty if no buttons pressed
# interpret result from python-lirc
if code:
code = code[0]
_LOGGER.info('Got new LIRC code %s', code)
self.hass.bus.fire(EVENT_IR_COMMAND_RECEIVED,
{BUTTON_NAME: code})
else:
time.sleep(0.2)
_LOGGER.info('LIRC interface thread stopped')

View File

@@ -15,12 +15,13 @@ import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, sun
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
HTTP_BAD_REQUEST, STATE_NOT_HOME, STATE_OFF, STATE_ON)
STATE_NOT_HOME, STATE_OFF, STATE_ON)
from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.core import State
from homeassistant.helpers.entity import split_entity_id
from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv
from homeassistant.components.http import HomeAssistantView
DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http']
@@ -76,34 +77,34 @@ def setup(hass, config):
message = template.render(hass, message)
log_entry(hass, name, message, domain, entity_id)
hass.http.register_path('GET', URL_LOGBOOK, _handle_get_logbook)
hass.wsgi.register_view(LogbookView)
hass.services.register(DOMAIN, 'log', log_message,
schema=LOG_MESSAGE_SCHEMA)
return True
def _handle_get_logbook(handler, path_match, data):
"""Return logbook entries."""
date_str = path_match.group('date')
class LogbookView(HomeAssistantView):
"""Handle logbook view requests."""
if date_str:
start_date = dt_util.parse_date(date_str)
url = '/api/logbook'
name = 'api:logbook'
extra_urls = ['/api/logbook/<date:date>']
if start_date is None:
handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST)
return
def get(self, request, date=None):
"""Retrieve logbook entries."""
if date:
start_day = dt_util.start_of_local_day(date)
else:
start_day = dt_util.start_of_local_day()
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)
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)))
events = recorder.query_events(
QUERY_EVENTS_BETWEEN,
(dt_util.as_utc(start_day), dt_util.as_utc(end_day)))
handler.write_json(humanify(events))
return self.json(humanify(events))
class Entry(object):

View File

@@ -0,0 +1,61 @@
"""
Support for sending data to Logentries webhook endpoint.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/logentries/
"""
import json
import logging
import requests
import homeassistant.util as util
from homeassistant.const import EVENT_STATE_CHANGED
from homeassistant.helpers import state as state_helper
from homeassistant.helpers import validate_config
_LOGGER = logging.getLogger(__name__)
DOMAIN = "logentries"
DEPENDENCIES = []
DEFAULT_HOST = 'https://webhook.logentries.com/noformat/logs/'
CONF_TOKEN = 'token'
def setup(hass, config):
"""Setup the Logentries component."""
if not validate_config(config, {DOMAIN: ['token']}, _LOGGER):
_LOGGER.error("Logentries token not present")
return False
conf = config[DOMAIN]
token = util.convert(conf.get(CONF_TOKEN), str)
le_wh = DEFAULT_HOST + token
def logentries_event_listener(event):
"""Listen for new messages on the bus and sends them to Logentries."""
state = event.data.get('new_state')
if state is None:
return
try:
_state = state_helper.state_as_number(state)
except ValueError:
_state = state.state
json_body = [
{
'domain': state.domain,
'entity_id': state.object_id,
'attributes': dict(state.attributes),
'time': str(event.time_fired),
'value': _state,
}
]
try:
payload = {"host": le_wh,
"event": json_body}
requests.post(le_wh, data=json.dumps(payload), timeout=10)
except requests.exceptions.RequestException as error:
_LOGGER.exception('Error sending to Logentries: %s', error)
hass.bus.listen(EVENT_STATE_CHANGED, logentries_event_listener)
return True

View File

@@ -36,6 +36,7 @@ DISCOVERY_PLATFORMS = {
discovery.SERVICE_PLEX: 'plex',
discovery.SERVICE_SQUEEZEBOX: 'squeezebox',
discovery.SERVICE_PANASONIC_VIERA: 'panasonic_viera',
discovery.SERVICE_ROKU: 'roku',
}
SERVICE_PLAY_MEDIA = 'play_media'
@@ -62,6 +63,7 @@ ATTR_APP_NAME = 'app_name'
ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands'
ATTR_INPUT_SOURCE = 'source'
ATTR_INPUT_SOURCE_LIST = 'source_list'
ATTR_MEDIA_ENQUEUE = 'enqueue'
MEDIA_TYPE_MUSIC = 'music'
MEDIA_TYPE_TVSHOW = 'tvshow'
@@ -144,6 +146,7 @@ MEDIA_PLAYER_MEDIA_SEEK_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string,
vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
ATTR_MEDIA_ENQUEUE: cv.boolean,
})
MEDIA_PLAYER_SELECT_SOURCE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
@@ -255,7 +258,7 @@ def media_seek(hass, position, entity_id=None):
hass.services.call(DOMAIN, SERVICE_MEDIA_SEEK, data)
def play_media(hass, media_type, media_id, entity_id=None):
def play_media(hass, media_type, media_id, entity_id=None, enqueue=None):
"""Send the media player the command for playing media."""
data = {ATTR_MEDIA_CONTENT_TYPE: media_type,
ATTR_MEDIA_CONTENT_ID: media_id}
@@ -263,6 +266,9 @@ def play_media(hass, media_type, media_id, entity_id=None):
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
if enqueue:
data[ATTR_MEDIA_ENQUEUE] = enqueue
hass.services.call(DOMAIN, SERVICE_PLAY_MEDIA, data)
@@ -363,9 +369,14 @@ def setup(hass, config):
"""Play specified media_id on the media player."""
media_type = service.data.get(ATTR_MEDIA_CONTENT_TYPE)
media_id = service.data.get(ATTR_MEDIA_CONTENT_ID)
enqueue = service.data.get(ATTR_MEDIA_ENQUEUE)
kwargs = {
ATTR_MEDIA_ENQUEUE: enqueue,
}
for player in component.extract_from_service(service):
player.play_media(media_type, media_id)
player.play_media(media_type, media_id, **kwargs)
if player.should_poll:
player.update_ha_state(True)

View File

@@ -253,7 +253,7 @@ class CastDevice(MediaPlayerDevice):
"""Seek the media to a specific location."""
self.cast.media_controller.seek(position)
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Play media from a URL."""
self.cast.media_controller.play_media(media_id, media_type)

View File

@@ -152,7 +152,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
"""Flag of media commands that are supported."""
return YOUTUBE_PLAYER_SUPPORT
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
self.youtube_id = media_id
self.update_ha_state()

View File

@@ -0,0 +1,158 @@
"""
Support for Google Play Music Desktop Player.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.gpmdp/
"""
import logging
import json
import socket
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
SUPPORT_PAUSE, MediaPlayerDevice)
from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['websocket-client==0.35.0']
SUPPORT_GPMDP = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the GPMDP platform."""
from websocket import create_connection
name = config.get("name", "GPM Desktop Player")
address = config.get("address")
if address is None:
_LOGGER.error("Missing address in config")
return False
add_devices([GPMDP(name, address, create_connection)])
class GPMDP(MediaPlayerDevice):
"""Representation of a GPMDP."""
# pylint: disable=too-many-public-methods, abstract-method
# pylint: disable=too-many-instance-attributes
def __init__(self, name, address, create_connection):
"""Initialize the media player."""
self._connection = create_connection
self._address = address
self._name = name
self._status = STATE_OFF
self._ws = None
self._title = None
self._artist = None
self._albumart = None
self.update()
def get_ws(self):
"""Check if the websocket is setup and connected."""
if self._ws is None:
try:
self._ws = self._connection(("ws://" + self._address +
":5672"), timeout=1)
except (socket.timeout, ConnectionRefusedError,
ConnectionResetError):
self._ws = None
elif self._ws.connected is True:
self._ws.close()
try:
self._ws = self._connection(("ws://" + self._address +
":5672"), timeout=1)
except (socket.timeout, ConnectionRefusedError,
ConnectionResetError):
self._ws = None
return self._ws
def update(self):
"""Get the latest details from the player."""
websocket = self.get_ws()
if websocket is None:
self._status = STATE_OFF
return
else:
state = websocket.recv()
state = ((json.loads(state))['payload'])
if state is True:
websocket.recv()
websocket.recv()
song = websocket.recv()
song = json.loads(song)
self._title = (song['payload']['title'])
self._artist = (song['payload']['artist'])
self._albumart = (song['payload']['albumArt'])
self._status = STATE_PLAYING
elif state is False:
self._status = STATE_PAUSED
@property
def media_content_type(self):
"""Content type of current playing media."""
return MEDIA_TYPE_MUSIC
@property
def state(self):
"""Return the state of the device."""
return self._status
@property
def media_title(self):
"""Title of current playing media."""
return self._title
@property
def media_artist(self):
"""Artist of current playing media (Music track only)."""
return self._artist
@property
def media_image_url(self):
"""Image url of current playing media."""
return self._albumart
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_GPMDP
def media_next_track(self):
"""Send media_next command to media player."""
websocket = self.get_ws()
if websocket is None:
return
websocket.send('{"namespace": "playback", "method": "forward"}')
def media_previous_track(self):
"""Send media_previous command to media player."""
websocket = self.get_ws()
if websocket is None:
return
websocket.send('{"namespace": "playback", "method": "rewind"}')
def media_play(self):
"""Send media_play command to media player."""
websocket = self.get_ws()
if websocket is None:
return
websocket.send('{"namespace": "playback", "method": "playPause"}')
self._status = STATE_PAUSED
self.update_ha_state()
def media_pause(self):
"""Send media_pause command to media player."""
websocket = self.get_ws()
if websocket is None:
return
websocket.send('{"namespace": "playback", "method": "playPause"}')
self._status = STATE_PAUSED
self.update_ha_state()

View File

@@ -320,7 +320,7 @@ class ItunesDevice(MediaPlayerDevice):
response = self.client.previous()
self.update_state(response)
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Send the play_media command to the media player."""
if media_type == MEDIA_TYPE_PLAYLIST:
response = self.client.play_playlist(media_id)

View File

@@ -10,7 +10,7 @@ import urllib
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
MediaPlayerDevice)
SUPPORT_TURN_OFF, MediaPlayerDevice)
from homeassistant.const import (
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
@@ -36,7 +36,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
url,
auth=(
config.get('user', ''),
config.get('password', ''))),
config.get('password', '')),
turn_off_action=config.get('turn_off_action', 'none')),
])
@@ -44,7 +45,8 @@ class KodiDevice(MediaPlayerDevice):
"""Representation of a XBMC/Kodi device."""
# pylint: disable=too-many-public-methods, abstract-method
def __init__(self, name, url, auth=None):
# pylint: disable=too-many-instance-attributes
def __init__(self, name, url, auth=None, turn_off_action=None):
"""Initialize the Kodi device."""
import jsonrpc_requests
self._name = name
@@ -52,6 +54,7 @@ class KodiDevice(MediaPlayerDevice):
self._server = jsonrpc_requests.Server(
'{}/jsonrpc'.format(self._url),
auth=auth)
self._turn_off_action = turn_off_action
self._players = list()
self._properties = None
self._item = None
@@ -181,11 +184,29 @@ class KodiDevice(MediaPlayerDevice):
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_KODI
supported_media_commands = SUPPORT_KODI
if self._turn_off_action in [
'quit', 'hibernate', 'suspend', 'reboot', 'shutdown']:
supported_media_commands |= SUPPORT_TURN_OFF
return supported_media_commands
def turn_off(self):
"""Turn off media player."""
self._server.System.Shutdown()
"""Execute turn_off_action to turn off media player."""
if self._turn_off_action == 'quit':
self._server.Application.Quit()
elif self._turn_off_action == 'hibernate':
self._server.System.Hibernate()
elif self._turn_off_action == 'suspend':
self._server.System.Suspend()
elif self._turn_off_action == 'reboot':
self._server.System.Reboot()
elif self._turn_off_action == 'shutdown':
self._server.System.Shutdown()
else:
_LOGGER.warning('turn_off requested but turn_off_action is none')
self.update_ha_state()
def volume_up(self):
@@ -278,6 +299,6 @@ class KodiDevice(MediaPlayerDevice):
self.update_ha_state()
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Send the play_media command to the media player."""
self._server.Player.Open({media_type: media_id}, {})

View File

@@ -0,0 +1,211 @@
"""
Support for LG TV running on NetCast 3 or 4.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.lg_netcast/
"""
from datetime import timedelta
import logging
from requests import RequestException
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
SUPPORT_SELECT_SOURCE, MEDIA_TYPE_CHANNEL, MediaPlayerDevice)
from homeassistant.const import (
CONF_PLATFORM, CONF_HOST, CONF_NAME, CONF_ACCESS_TOKEN,
STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN)
import homeassistant.util as util
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['https://github.com/wokar/pylgnetcast/archive/'
'v0.2.0.zip#pylgnetcast==0.2.0']
SUPPORT_LGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
DEFAULT_NAME = 'LG TV Remote'
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): "lg_netcast",
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_ACCESS_TOKEN, default=None):
vol.All(cv.string, vol.Length(max=6)),
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the LG TV platform."""
from pylgnetcast import LgNetCastClient
client = LgNetCastClient(config[CONF_HOST], config[CONF_ACCESS_TOKEN])
add_devices([LgTVDevice(client, config[CONF_NAME])])
# pylint: disable=too-many-public-methods, abstract-method
# pylint: disable=too-many-instance-attributes
class LgTVDevice(MediaPlayerDevice):
"""Representation of a LG TV."""
def __init__(self, client, name):
"""Initialize the LG TV device."""
self._client = client
self._name = name
self._muted = False
# Assume that the TV is in Play mode
self._playing = True
self._volume = 0
self._channel_name = ''
self._program_name = ''
self._state = STATE_UNKNOWN
self._sources = {}
self._source_names = []
self.update()
def send_command(self, command):
"""Send remote control commands to the TV."""
from pylgnetcast import LgNetCastError
try:
with self._client as client:
client.send_command(command)
except (LgNetCastError, RequestException):
self._state = STATE_OFF
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update(self):
"""Retrieve the latest data from the LG TV."""
from pylgnetcast import LgNetCastError
try:
with self._client as client:
self._state = STATE_PLAYING
volume_info = client.query_data('volume_info')
if volume_info:
volume_info = volume_info[0]
self._volume = float(volume_info.find('level').text)
self._muted = volume_info.find('mute').text == 'true'
channel_info = client.query_data('cur_channel')
if channel_info:
channel_info = channel_info[0]
self._channel_name = channel_info.find('chname').text
self._program_name = channel_info.find('progName').text
channel_list = client.query_data('channel_list')
if channel_list:
channel_names = [str(c.find('chname').text) for
c in channel_list]
self._sources = dict(zip(channel_names, channel_list))
# sort source names by the major channel number
source_tuples = [(k, self._sources[k].find('major').text)
for k in self._sources.keys()]
sorted_sources = sorted(
source_tuples, key=lambda channel: int(channel[1]))
self._source_names = [n for n, k in sorted_sources]
except (LgNetCastError, RequestException):
self._state = STATE_OFF
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._muted
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return self._volume / 100.0
@property
def source(self):
"""Return the current input source."""
return self._channel_name
@property
def source_list(self):
"""List of available input sources."""
return self._source_names
@property
def media_content_type(self):
"""Content type of current playing media."""
return MEDIA_TYPE_CHANNEL
@property
def media_channel(self):
"""Channel currently playing."""
return self._channel_name
@property
def media_title(self):
"""Title of current playing media."""
return self._program_name
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_LGTV
def turn_off(self):
"""Turn off media player."""
self.send_command(1)
def volume_up(self):
"""Volume up the media player."""
self.send_command(24)
def volume_down(self):
"""Volume down media player."""
self.send_command(25)
def mute_volume(self, mute):
"""Send mute command."""
self.send_command(26)
def select_source(self, source):
"""Select input source."""
self._client.change_channel(self._sources[source])
def media_play_pause(self):
"""Simulate play pause media player."""
if self._playing:
self.media_pause()
else:
self.media_play()
def media_play(self):
"""Send play command."""
self._playing = True
self._state = STATE_PLAYING
self.send_command(33)
def media_pause(self):
"""Send media pause command to media player."""
self._playing = False
self._state = STATE_PAUSED
self.send_command(34)
def media_next_track(self):
"""Send next track command."""
self.send_command(36)
def media_previous_track(self):
"""Send the previous track command."""
self.send_command(37)

View File

@@ -89,7 +89,13 @@ class MpdDevice(MediaPlayerDevice):
try:
self.status = self.client.status()
self.currentsong = self.client.currentsong()
except mpd.ConnectionError:
except (mpd.ConnectionError, BrokenPipeError, ValueError):
# Cleanly disconnect in case connection is not in valid state
try:
self.client.disconnect()
except mpd.ConnectionError:
pass
self.client.connect(self.server, self.port)
if self.password is not None:
@@ -206,7 +212,7 @@ class MpdDevice(MediaPlayerDevice):
"""Service to send the MPD the command for previous track."""
self.client.previous()
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Send the media player the command for playing a playlist."""
_LOGGER.info(str.format("Playing playlist: {0}", media_id))
if media_type == MEDIA_TYPE_PLAYLIST:

View File

@@ -9,7 +9,7 @@ import logging
from homeassistant.components.media_player import (
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.const import STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME
REQUIREMENTS = ['https://github.com/danieljkemp/onkyo-eiscp/archive/'
'python3.zip#onkyo-eiscp==0.9.2']
@@ -17,29 +17,59 @@ _LOGGER = logging.getLogger(__name__)
SUPPORT_ONKYO = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
KNOWN_HOSTS = []
DEFAULT_SOURCES = {"tv": "TV", "bd": "Bluray", "game": "Game", "aux1": "Aux1",
"video1": "Video 1", "video2": "Video 2",
"video3": "Video 3", "video4": "Video 4",
"video5": "Video 5", "video6": "Video 6",
"video7": "Video 7"}
CONFIG_SOURCE_LIST = "sources"
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Onkyo platform."""
import eiscp
from eiscp import eISCP
add_devices(OnkyoDevice(receiver)
for receiver in eISCP.discover())
hosts = []
if CONF_HOST in config and config[CONF_HOST] not in KNOWN_HOSTS:
try:
hosts.append(OnkyoDevice(eiscp.eISCP(config[CONF_HOST]),
config.get(CONFIG_SOURCE_LIST,
DEFAULT_SOURCES),
name=config[CONF_NAME]))
KNOWN_HOSTS.append(config[CONF_HOST])
except OSError:
_LOGGER.error('Unable to connect to receiver at %s.',
config[CONF_HOST])
else:
for receiver in eISCP.discover():
if receiver.host not in KNOWN_HOSTS:
hosts.append(OnkyoDevice(receiver,
config.get(CONFIG_SOURCE_LIST,
DEFAULT_SOURCES)))
KNOWN_HOSTS.append(receiver.host)
add_devices(hosts)
# pylint: disable=too-many-instance-attributes
class OnkyoDevice(MediaPlayerDevice):
"""Representation of a Onkyo device."""
# pylint: disable=too-many-public-methods, abstract-method
def __init__(self, receiver):
def __init__(self, receiver, sources, name=None):
"""Initialize the Onkyo Receiver."""
self._receiver = receiver
self._muted = False
self._volume = 0
self._pwstate = STATE_OFF
self.update()
self._name = '{}_{}'.format(
self._name = name or '{}_{}'.format(
receiver.info['model_name'], receiver.info['identifier'])
self._current_source = None
self._source_list = list(sources.values())
self._source_mapping = sources
self._reverse_mapping = {value: key for key, value in sources.items()}
self.update()
def update(self):
"""Get the latest details from the device."""
@@ -52,8 +82,13 @@ class OnkyoDevice(MediaPlayerDevice):
volume_raw = self._receiver.command('volume query')
mute_raw = self._receiver.command('audio-muting query')
current_source_raw = self._receiver.command('input-selector query')
self._current_source = '_'.join('_'.join(
[i for i in current_source_raw[1]]))
for source in current_source_raw[1]:
if source in self._source_mapping:
self._current_source = self._source_mapping[source]
break
else:
self._current_source = '_'.join(
[i for i in current_source_raw[1]])
self._muted = bool(mute_raw[1] == 'on')
self._volume = int(volume_raw[1], 16)/80.0
@@ -87,6 +122,11 @@ class OnkyoDevice(MediaPlayerDevice):
""""Return the current input source of the device."""
return self._current_source
@property
def source_list(self):
"""List of available input sources."""
return self._source_list
def turn_off(self):
"""Turn off media player."""
self._receiver.command('system-power standby')
@@ -108,4 +148,6 @@ class OnkyoDevice(MediaPlayerDevice):
def select_source(self, source):
"""Set the input source."""
if source in self._source_list:
source = self._reverse_mapping[source]
self._receiver.command('input-selector {}'.format(source))

View File

@@ -0,0 +1,205 @@
"""
Support for the roku media player.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.roku/
"""
import logging
from homeassistant.components.media_player import (
MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, STATE_HOME)
REQUIREMENTS = [
'https://github.com/bah2830/python-roku/archive/3.1.1.zip'
'#python-roku==3.1.1']
KNOWN_HOSTS = []
DEFAULT_PORT = 8060
_LOGGER = logging.getLogger(__name__)
SUPPORT_ROKU = SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\
SUPPORT_PLAY_MEDIA | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
SUPPORT_SELECT_SOURCE
# pylint: disable=abstract-method
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Roku platform."""
hosts = []
if discovery_info and discovery_info in KNOWN_HOSTS:
return
if discovery_info is not None:
_LOGGER.debug('Discovered Roku: %s', discovery_info[0])
hosts.append(discovery_info[0])
elif CONF_HOST in config:
hosts.append(config[CONF_HOST])
rokus = []
for host in hosts:
rokus.append(RokuDevice(host))
KNOWN_HOSTS.append(host)
add_devices(rokus)
class RokuDevice(MediaPlayerDevice):
"""Representation of a Roku device on the network."""
# pylint: disable=abstract-method
# pylint: disable=too-many-public-methods
def __init__(self, host):
"""Initialize the Roku device."""
from roku import Roku
self.roku = Roku(host)
self.update()
def update(self):
"""Retrieve latest state."""
import requests.exceptions
try:
self.roku_name = "roku_" + self.roku.device_info.sernum
self.ip_address = self.roku.host
self.channels = self.get_source_list()
if self.roku.current_app is not None:
self.current_app = self.roku.current_app
else:
self.current_app = None
except requests.exceptions.ConnectionError:
self.current_app = None
def get_source_list(self):
"""Get the list of applications to be used as sources."""
return ["Home"] + sorted(channel.name for channel in self.roku.apps)
@property
def should_poll(self):
"""Device should be polled."""
return True
@property
def name(self):
"""Return the name of the device."""
return self.roku_name
@property
def state(self):
"""Return the state of the device."""
if self.current_app is None:
return STATE_UNKNOWN
if self.current_app.name in ["Power Saver", "Default screensaver"]:
return STATE_IDLE
elif self.current_app.name == "Roku":
return STATE_HOME
elif self.current_app.name is not None:
return STATE_PLAYING
return STATE_UNKNOWN
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_ROKU
@property
def media_content_type(self):
"""Content type of current playing media."""
if self.current_app is None:
return None
elif self.current_app.name == "Power Saver":
return None
elif self.current_app.name == "Roku":
return None
else:
return MEDIA_TYPE_VIDEO
@property
def media_image_url(self):
"""Image url of current playing media."""
if self.current_app is None:
return None
elif self.current_app.name == "Roku":
return None
elif self.current_app.name == "Power Saver":
return None
elif self.current_app.id is None:
return None
return 'http://{0}:{1}/query/icon/{2}'.format(self.ip_address,
DEFAULT_PORT,
self.current_app.id)
@property
def app_name(self):
"""Name of the current running app."""
if self.current_app is not None:
return self.current_app.name
@property
def app_id(self):
"""Return the ID of the current running app."""
if self.current_app is not None:
return self.current_app.id
@property
def source(self):
"""Return the current input source."""
if self.current_app is not None:
return self.current_app.name
@property
def source_list(self):
"""List of available input sources."""
return self.channels
def media_play_pause(self):
"""Send play/pause command."""
if self.current_app is not None:
self.roku.play()
def media_previous_track(self):
"""Send previous track command."""
if self.current_app is not None:
self.roku.reverse()
def media_next_track(self):
"""Send next track command."""
if self.current_app is not None:
self.roku.forward()
def mute_volume(self, mute):
"""Mute the volume."""
if self.current_app is not None:
self.roku.volume_mute()
def volume_up(self):
"""Volume up media player."""
if self.current_app is not None:
self.roku.volume_up()
def volume_down(self):
"""Volume down media player."""
if self.current_app is not None:
self.roku.volume_down()
def select_source(self, source):
"""Select input source."""
if self.current_app is not None:
if source == "Home":
self.roku.home()
else:
channel = self.roku[source]
channel.launch()

View File

@@ -145,3 +145,11 @@ select_source:
source:
description: Name of the source to switch to. Platform dependent.
example: 'video1'
sonos_group_players:
description: Send Sonos media player the command for grouping all players into one (party mode).
fields:
entity_id:
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
example: 'media_player.living_room_sonos'

View File

@@ -7,14 +7,15 @@ https://home-assistant.io/components/media_player.sonos/
import datetime
import logging
import socket
from os import path
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
MediaPlayerDevice)
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF)
from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['SoCo==0.11.1']
@@ -32,6 +33,8 @@ SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA |\
SUPPORT_SEEK
SERVICE_GROUP_PLAYERS = 'sonos_group_players'
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -63,9 +66,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.warning('No Sonos speakers found.')
return False
add_devices(SonosDevice(hass, p) for p in players)
devices = [SonosDevice(hass, p) for p in players]
add_devices(devices)
_LOGGER.info('Added %s Sonos speakers', len(players))
def group_players_service(service):
"""Group media players, use player as coordinator."""
entity_id = service.data.get('entity_id')
if entity_id:
_devices = [device for device in devices
if device.entity_id == entity_id]
else:
_devices = devices
for device in _devices:
device.group_players()
device.update_ha_state(True)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_GROUP_PLAYERS,
group_players_service,
descriptions.get(SERVICE_GROUP_PLAYERS))
return True
@@ -74,16 +99,26 @@ def only_if_coordinator(func):
If used as decorator, avoid calling the decorated method if player is not
a coordinator. If not, a grouped speaker (not in coordinator role) will
throw soco.exceptions.SoCoSlaveException
throw soco.exceptions.SoCoSlaveException.
Also, partially catch exceptions like:
soco.exceptions.SoCoUPnPException: UPnP Error 701 received:
Transition not available from <player ip address>
"""
def wrapper(*args, **kwargs):
"""Decorator wrapper."""
if args[0].is_coordinator:
return func(*args, **kwargs)
from soco.exceptions import SoCoUPnPException
try:
func(*args, **kwargs)
except SoCoUPnPException:
_LOGGER.error('command "%s" for Sonos device "%s" '
'not available in this mode',
func.__name__, args[0].name)
else:
_LOGGER.debug('Ignore command "%s" for Sonos device "%s" '
'(not coordinator)',
func.__name__, args[0].name)
_LOGGER.debug('Ignore command "%s" for Sonos device "%s" (%s)',
func.__name__, args[0].name, 'not coordinator')
return wrapper
@@ -104,7 +139,7 @@ class SonosDevice(MediaPlayerDevice):
@property
def should_poll(self):
"""No polling needed."""
"""Polling needed."""
return True
def update_sonos(self, now):
@@ -258,9 +293,27 @@ class SonosDevice(MediaPlayerDevice):
self._player.play()
@only_if_coordinator
def play_media(self, media_type, media_id):
"""Send the play_media command to the media player."""
self._player.play_uri(media_id)
def play_media(self, media_type, media_id, **kwargs):
"""
Send the play_media command to the media player.
If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue.
"""
if kwargs.get(ATTR_MEDIA_ENQUEUE):
from soco.exceptions import SoCoUPnPException
try:
self._player.add_uri_to_queue(media_id)
except SoCoUPnPException:
_LOGGER.error('Error parsing media uri "%s", '
"please check it's a valid media resource "
'supported by Sonos', media_id)
else:
self._player.play_uri(media_id)
@only_if_coordinator
def group_players(self):
"""Group all players under this coordinator."""
self._player.partymode()
@property
def available(self):

View File

@@ -402,7 +402,7 @@ class UniversalMediaPlayer(MediaPlayerDevice):
data = {ATTR_MEDIA_SEEK_POSITION: position}
self._call_service(SERVICE_MEDIA_SEEK, data)
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
data = {ATTR_MEDIA_CONTENT_TYPE: media_type,
ATTR_MEDIA_CONTENT_ID: media_id}

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