Compare commits

..

208 Commits

Author SHA1 Message Date
Paulus Schoutsen 8c505e625b Merge pull request #2323 from home-assistant/dev
0.22
2016-06-18 13:20:51 -07:00
Paulus Schoutsen 314fa42298 Version bump to 0.22 2016-06-18 13:19:57 -07:00
Paulus Schoutsen a80a74b586 Add camera timeouts 2016-06-18 13:06:14 -07:00
Paulus Schoutsen 2508e9f9ff Add timeout to mjpeg streams 2016-06-18 12:34:39 -07:00
Paulus Schoutsen 71157dbec9 Merge branch 'master' into dev
Conflicts:
	homeassistant/components/frontend/version.py
	homeassistant/components/frontend/www_static/core.js.gz
	homeassistant/components/frontend/www_static/frontend.html
	homeassistant/components/frontend/www_static/frontend.html.gz
	homeassistant/components/frontend/www_static/service_worker.js
	homeassistant/components/frontend/www_static/service_worker.js.gz
	homeassistant/const.py
2016-06-18 12:00:38 -07:00
devdelay 1f7792678b Add service set_hvac_mode (#2303)
* set hvac_mode

* Update __init__.py

* Update __init__.py
2016-06-18 10:20:39 -07:00
Phil Kates 40840044ca Wink Rollershutter (#2294)
* Update python-wink to 0.7.7

* Add Wink Rollershutter component
2016-06-18 09:59:13 -07:00
Nick Touran 2882f05f2c Added template rendering to shell_command component (#2268)
* Added template rendering to `shell_command` component

* Security upgrades to template rendering in shell_command.

* Added new unit tests for shell_command templates.
Better failure when template is invalid in shell_command
2016-06-18 09:57:18 -07:00
Fabian Affolter b646accf87 Catch ValueError (#2296)
* Catch ValueError

* Less options and don't use state
2016-06-18 09:48:32 -07:00
Nick Touran e7ea6ecf5a Better handling for when user hasn't properly configured Pandora client (#2317) 2016-06-18 08:23:35 -07:00
Paulus Schoutsen 29343ad651 Fix pep257 bt home hub 5 test 2016-06-18 08:20:14 -07:00
Fabian Affolter 28d86207e1 Add support for hydrological data from FOEN (#2318) 2016-06-18 08:18:48 -07:00
Fabian Affolter 6a01227635 Upgrade python-telegram-bot to 4.2.1 (#2319) 2016-06-18 08:16:28 -07:00
Nolan Gilley b6fb21edaf Plex sensor (#2210)
add option to name in config

fix const import

use plexapi

add myplex support for remote access

use first server if server not specified

use list comprehension

use dictionary comprehension
2016-06-14 23:07:00 -07:00
Paulus Schoutsen a65a122464 Fix discovery (#2305) 2016-06-14 22:51:46 -07:00
Nick Touran 5c601f1d5f Stability improvement in Pandora and proper shutdown in LIRC (#2299)
* Pandora cleanups and enhancements

Added media_content_type
reduced debug messages
made more robust station list
Eliminated auto-pause detection issue

* Added proper de-init of LIRC

* Now won't re-spawn Pandora client if turn_on command is sent twice
2016-06-14 22:42:54 -07:00
Lewis Juggins 7b8b78ec0e BT Home Hub 5 device tracker support (#2250) 2016-06-14 22:41:49 -07:00
Per Sandström 38030fcfca ASUSWRT Autodetect protocol (#2300) 2016-06-14 22:17:32 -07:00
Paulus Schoutsen 39913075f4 Fix Locative view name 2016-06-14 22:12:44 -07:00
Paulus Schoutsen 2036c44364 Hotfix 21 2 (#2302)
* Update frontend

Conflicts:
	homeassistant/components/frontend/version.py
	homeassistant/components/frontend/www_static/core.js.gz
	homeassistant/components/frontend/www_static/frontend.html
	homeassistant/components/frontend/www_static/frontend.html.gz
	homeassistant/components/frontend/www_static/home-assistant-polymer
	homeassistant/components/frontend/www_static/service_worker.js
	homeassistant/components/frontend/www_static/service_worker.js.gz

* Add a default OPTIONS handler for wsgi (#2301)

When a browser makes a CORS request, it often makes a 'preflight'
options request in order to make sure the resource is valid, and that
it has the right CORS access. This adds a default OPTIONS handler for
all views. If a view needs to customize the OPTIONS handler for some
reason, it's free to, but this way CORS will work.

* Version bump to 0.21.2
2016-06-14 19:54:09 -07:00
Josh Wright 3fcc07af04 Add a default OPTIONS handler for wsgi (#2301)
When a browser makes a CORS request, it often makes a 'preflight'
options request in order to make sure the resource is valid, and that
it has the right CORS access. This adds a default OPTIONS handler for
all views. If a view needs to customize the OPTIONS handler for some
reason, it's free to, but this way CORS will work.
2016-06-14 19:44:12 -07:00
Paulus Schoutsen 65750f667b Update frontend 2016-06-14 18:39:44 -07:00
Per Sandström f07ba1e9a6 Merge pull request #2298 from persandstrom/verisure_lower_severity_of_message
lower severity of non critical error
2016-06-14 20:56:49 +02:00
Per Sandström 6e5e0e7acc lower severity of non critical error 2016-06-14 20:21:42 +02:00
Paulus Schoutsen 9d7c9d1262 Update frontend 2016-06-13 20:11:01 -07:00
Paulus Schoutsen 42c5475284 Fix Wink discovery 2016-06-13 20:06:32 -07:00
Edward Romano 8e839be938 Refactor Forecast.io (#2217)
* Refactor Forecast.io

* Some more refactoring and code review workoff

* Dict switch refactor

* CamelCase for data lookup

* Fixing unit_of_measure update

* Better default return for unit_of_measurement

* Test fix
2016-06-13 18:54:49 -07:00
Paulus Schoutsen ab48010d14 Add 1024x1024 favicon 2016-06-13 00:04:54 -07:00
Matthew Treinish 1381984b77 Add ssh public key support to the asuswrt component (#2287)
The pexpect.pxssh module has support for using public key
authentication. [1] This commit adds support for leveraging that and
establishing a ssh connection with a public key instead of a password.

[1] http://pexpect.readthedocs.io/en/stable/api/pxssh.html#pexpect.pxssh.pxssh.login
2016-06-12 21:27:41 -07:00
Paulus Schoutsen 6dcf3682df Tweak event helper 2016-06-12 20:37:37 -07:00
Nick Touran 65d1f7af50 Added Pandora radio media player (#2274)
* Added Pandora media player utilizing the Pianobar client

* Added Pandora to .coveragerc ignore

* Fixes some docstring formats in Pandora

* More minor formatting tweaks for Pandora

* Eliminated non-portable assumption from Pandora component

* Updated Pandora to properly update currently-playing song.

* Docstring fixes in Pandora

* Added check to ensure Pianobar client is available in path for Pandora.

* Made Pandora client verification a function instead of method.

* Better handling of dependency verification in Pandora.
2016-06-12 18:35:12 -07:00
Jesse Zoldak 16f4695a13 Add tests for forecast.io (#2227)
* Add tests for forecast.io

* Fix linting items and don't call a platform a component
2016-06-12 17:22:58 -07:00
Landrash c7ee74a573 Local file - Camera platform (#2282) 2016-06-12 16:26:29 -07:00
arsaboo 8e2c1ff4aa Include the Voltage sensor (#2285)
The API provides the voltage information and will be useful for people to troubleshoot their BloomSky.
2016-06-12 16:19:13 -07:00
thejacko12354 e437151881 Update samsungtv.py (#2286)
Changed line 75 'KEY_POWER' to 'KEY'
Fixes the problem that every 10 sec the tv interprets that the Up-button is pressed
2016-06-12 16:03:40 -07:00
Martin Hjelmare 81ca175906 Add mysensors IR switch device and service (#2239)
* Add mysensors IR switch device and service

* Add MySensorsIRSwitch as child class to MySensorsSwitch.
* Add platform specific service mysensors_send_ir_code. Only call
	device method in service function if device is IR device.
* Add service and required attribute to state helper to support scenes.
* Move V_IR_SEND type from sensor.mysensors to switch.mysensors
	platform.
* Populate switch.services.yaml with service descriptions.

* Fix check of entity_id in service function

Since multiple entity_ids can be passed as service data, and the
entity_id service attribute is forced to a list by the service
validation schema, the check in the service function should iterate
over any entity ids.
2016-06-12 23:04:45 +02:00
Paulus Schoutsen ebe4c39020 Merge branch '0-21-1' into dev
Conflicts:
	homeassistant/components/frontend/www_static/core.js.gz
	homeassistant/components/frontend/www_static/frontend.html.gz
	homeassistant/components/frontend/www_static/service_worker.js.gz
	homeassistant/const.py
	requirements_all.txt
	setup.py
2016-06-12 00:25:36 -07:00
Paulus Schoutsen 952afeb717 Merge pull request #2281 from home-assistant/0-21-1
Hotfix 0.21.1
2016-06-12 00:23:03 -07:00
Paulus Schoutsen 40be883c0e version bump to 0.21.1 2016-06-12 00:06:37 -07:00
Paulus Schoutsen 5c87883c86 Update frontend 2016-06-12 00:04:37 -07:00
St. John Johnson b2b1804f5e Fixing MJPEG streaming in Werkzeug by taking advantage of direct_passthrough (#2277) 2016-06-12 00:03:18 -07:00
Paulus Schoutsen f5fc4cd97f Alexa: run script before generating response text (#2276) 2016-06-12 00:03:18 -07:00
Paulus Schoutsen bc78997bbd Bugfixes random (#2270)
* Fix Z-Wave autoheal network

* Make config_per_platform handle bad config better
2016-06-12 00:03:18 -07:00
Paulus Schoutsen 35dd3b8d0d Update screenshot README 2016-06-12 00:03:17 -07:00
Nick Touran 491c06f53b Recover from rare error condition from LIRC (#2267)
* More resilient accessing of LIRC codes to handle rare error case.

* Line length fix in LIRC
2016-06-12 00:03:17 -07:00
Gergely Imreh 31c1b7f6ad sensor/gtfs: add sanity check, origin earlier than destination (#2265)
Previously experienced issues on routes where services operate in both
directions. The query picked up not just paths where service goes
from Origin ->  Destination, but trips going Destination -> Origin,
and shown bogus results.

Ensure that this doesn't happen by requiring the origin station's
stop_sequence value to be lower than the destination station.
2016-06-12 00:03:17 -07:00
Paulus Schoutsen da5b50848a Add eventlet to base requirements (#2264)
Conflicts:
	requirements_all.txt
	setup.py
2016-06-12 00:02:58 -07:00
Paulus Schoutsen 586f69ac95 Update frontend 2016-06-11 23:57:24 -07:00
St. John Johnson 3723c3a7e8 Fixing MJPEG streaming in Werkzeug by taking advantage of direct_passthrough (#2277) 2016-06-11 20:50:10 -07:00
Paulus Schoutsen 145c98c40c Alexa: run script before generating response text (#2276) 2016-06-11 17:57:04 -07:00
Paulus Schoutsen 30f74bb3ca Migrate to generic discovery method (#2271)
* Migrate to generic discovery method

* Add tests for discovery
2016-06-11 17:43:13 -07:00
Paulus Schoutsen c9756c40e2 Bugfixes random (#2270)
* Fix Z-Wave autoheal network

* Make config_per_platform handle bad config better
2016-06-10 22:53:31 -07:00
Paulus Schoutsen b60806583c Update asuswrt.py 2016-06-10 21:14:11 -07:00
Michaël Arnauts 868c08e34b Add stop command to google cast component (#2269)
* Add stop command to google cast component

* Add SUPPORT_STOP capabilities to google cast component
2016-06-10 21:12:50 -07:00
Paulus Schoutsen 71eb09ee5e Fix configurator tests 2016-06-10 20:50:04 -07:00
Paulus Schoutsen 809e613148 Update frontend 2016-06-10 19:45:15 -07:00
Paulus Schoutsen 0dbc023f5b Fix lint errors 2016-06-09 23:41:26 -07:00
Joseph Piron b6d75e6c5a Netio Switch platform support (#2181)
* WSGI based request handler

with a bit of polishing

Signed-off-by: eagleamon <joseph.piron@gmail.com>

* removed stale comment and fixed version, but failed tests do not seem to be related

* removing the wrapper hack

* added in requirements file

* Found the caved in lint error..
2016-06-09 23:40:14 -07:00
wind-rider c78e6c088e Add a swagger.yaml file (#2182)
* Add a swagger.yaml file

@balloob
I created a swagger configuration file that will help people create clients (apps / frontends) for Home Assistant more easily. Based upon this code it is even possible to generate client code for several programming languages.

I created it by hand now, so when the API changes it will need to be updated. That's why it would be better to generate this specification automatically. This is possible for API frameworks but I don't know whether it is possible for the handwritten endpoints in Home Assistant. Maybe you could assist here?

This documentation could be used to replace a part of https://home-assistant.io/developers/rest_api/.

* Added restrict parameter

* Moved swagger file to docs folder
2016-06-09 23:35:47 -07:00
Thiago Oliveira 02f342b670 add fan_min_on_time service to ecobee (#2159) 2016-06-09 23:34:29 -07:00
Hugo Dupras 213a738240 Add Netatmo component and add support for Netatmo Welcome Camera (#2233)
* Introducing the Netatmo component

As Netatmo is providing several type of device (sensor, camera), a new Netatmo
component needs to be created in order to centralize the Netatmo login data.
Currently this change only impacts the Netatmo Weather station

* Add new Netatmo library

This new API will provide access to the Welcome Camera

* Basic support for Netatmo Welcome camera

This change introduces support for Netatmo Welcome camera. Currently, it will
add all detected camera to Home Assistant, camera filtering (similar to the one
used for weather station modules) will be added later

* Remove useless REQUIREMENTS

* Fixes for Netatmo Welcome support

* Allow to filter Welcome cameras by name and/or home

* Update requirements for Netatmo components

* Fix multi-camera support for Welcome

* Fix pep8 error/warning

* This commit also adds improved logging for bad credentials

* Add Throttle decorator for Welcome update function

As the update function updates the data for all cameras, we should prevent this
function to be called several time during an interval
2016-06-09 23:31:36 -07:00
Paulus Schoutsen e4fe8336cc Update frontend 2016-06-09 23:27:35 -07:00
Paulus Schoutsen 068e62623d Update frontend 2016-06-09 22:12:45 -07:00
Jeffrey Lin 30f5727b40 Added support for AP mode in asuswrt (#2263)
* Added support for AP mode in asuswrt

* Corrected number of return values in asuswrt
2016-06-09 21:30:47 -07:00
Paulus Schoutsen 815a6999b1 Update screenshot README 2016-06-09 21:23:20 -07:00
Nick Touran c229d9e90f Recover from rare error condition from LIRC (#2267)
* More resilient accessing of LIRC codes to handle rare error case.

* Line length fix in LIRC
2016-06-09 20:53:41 -07:00
Gergely Imreh abc353c083 sensor/gtfs: add sanity check, origin earlier than destination (#2265)
Previously experienced issues on routes where services operate in both
directions. The query picked up not just paths where service goes
from Origin ->  Destination, but trips going Destination -> Origin,
and shown bogus results.

Ensure that this doesn't happen by requiring the origin station's
stop_sequence value to be lower than the destination station.
2016-06-09 20:48:12 -07:00
Paulus Schoutsen 38639d26ea Add eventlet to base requirements (#2264) 2016-06-09 18:47:35 -07:00
Hugo Dupras 1c637558bf Round download speed for nzbget sensor (#2255) 2016-06-09 08:06:01 -07:00
mikebarris 5223d20668 Removed webcolors dependency in favor of dictionary lookup. (#2215)
* Removed webcolors dependency in favor of dictionary lookup.

* Fixed code style errors.

* Moved color dictionary to module per suggestion.

* Removed try/except per suggestion.
2016-06-08 22:25:32 -07:00
Dan Sullivan ce829d194c Added Sonos snapshot feature (#2240)
* Added Sonos snapshot feature

* Fix lint errors

* Use snake case

* Import dependency in a method
2016-06-08 21:47:49 -07:00
srirams 4a5ad24ae0 fix zwave thermostat with multiple setpoints (#2237)
* fix zwave thermostat with multiple setpoints

* fix zwave thermostat with multiple setpoints
2016-06-08 21:39:44 -07:00
Fabian Affolter 33cb1b3be6 SNMP sensor (#2244)
* Add snmp sensor

* Add ATTR_UNIT_OF_MEASUREMENT
2016-06-08 21:16:43 -07:00
Paulus Schoutsen 0525af920c Update betamax casettes 2016-06-08 21:06:14 -07:00
Fabian Affolter 831799a7af Upgrade betamax to 0.7.0 2016-06-08 21:06:14 -07:00
Fabian Affolter 8e5da5776d Add missing key 'forecast' (#2256) 2016-06-08 20:59:20 -07:00
Fabian Affolter be9730cc6c Upgrade astral to 1.2 (#2259) 2016-06-08 20:58:16 -07:00
Daniel Høyer Iversen e44c2a4016 Improve config validation for group (#2206)
* Improve config validation if invalid entity for groups

* Improve error message when entity id is invalid
2016-06-08 20:55:08 -07:00
Paulus Schoutsen 29ffa5c282 Version bump to 0.22.0.dev0 2016-06-07 19:28:13 -07:00
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
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
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
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
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
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
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
216 changed files with 9084 additions and 2368 deletions
+15 -1
View File
@@ -75,6 +75,12 @@ omit =
homeassistant/components/zwave.py
homeassistant/components/*/zwave.py
homeassistant/components/enocean.py
homeassistant/components/*/enocean.py
homeassistant/components/netatmo.py
homeassistant/components/*/netatmo.py
homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/binary_sensor/arest.py
@@ -89,6 +95,7 @@ omit =
homeassistant/components/device_tracker/aruba.py
homeassistant/components/device_tracker/asuswrt.py
homeassistant/components/device_tracker/bluetooth_tracker.py
homeassistant/components/device_tracker/bt_home_hub_5.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/icloud.py
@@ -111,6 +118,8 @@ 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
@@ -121,6 +130,7 @@ omit =
homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/onkyo.py
homeassistant/components/media_player/panasonic_viera.py
homeassistant/components/media_player/pandora.py
homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/plex.py
homeassistant/components/media_player/roku.py
@@ -156,6 +166,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
@@ -165,16 +176,18 @@ omit =
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
homeassistant/components/sensor/nzbget.py
homeassistant/components/sensor/onewire.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/plex.py
homeassistant/components/sensor/rest.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/snmp.py
homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/steam_online.py
homeassistant/components/sensor/supervisord.py
homeassistant/components/sensor/swiss_hydrological_data.py
homeassistant/components/sensor/swiss_public_transport.py
homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/temper.py
@@ -190,6 +203,7 @@ omit =
homeassistant/components/switch/edimax.py
homeassistant/components/switch/hikvisioncam.py
homeassistant/components/switch/mystrom.py
homeassistant/components/switch/netio.py
homeassistant/components/switch/orvibo.py
homeassistant/components/switch/pulseaudio_loopback.py
homeassistant/components/switch/rest.py
+1 -1
View File
@@ -1,7 +1,7 @@
**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#
+5
View File
@@ -85,3 +85,8 @@ venv
*.swo
ctags.tmp
# vagrant stuff
virtualization/vagrant/setup_done
virtualization/vagrant/.vagrant
virtualization/vagrant/config
+2 -9
View File
@@ -19,15 +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
RUN wget http://www.openssl.org/source/openssl-1.0.2h.tar.gz && \
tar -xvzf openssl-1.0.2h.tar.gz && \
cd openssl-1.0.2h && \
./config --prefix=/usr/ && \
make && \
make install && \
rm -rf openssl-1.0.2h*
# certifi breaks Debian based installs
RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi
# Copy source
COPY . .
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 232 KiB

+567
View File
@@ -0,0 +1,567 @@
swagger: '2.0'
info:
title: Home Assistant
description: Home Assistant REST API
version: "1.0.0"
# the domain of the service
host: localhost:8123
# array of all schemes that your API supports
schemes:
- http
- https
securityDefinitions:
api_key:
type: apiKey
description: API password
name: api_password
in: query
# api_key:
# type: apiKey
# description: API password
# name: x-ha-access
# in: header
# will be prefixed to all paths
basePath: /api
consumes:
- application/json
produces:
- application/json
paths:
/:
get:
summary: API alive message
description: Returns message if API is up and running.
tags:
- Core
responses:
200:
description: API is up and running
schema:
$ref: '#/definitions/Message'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/config:
get:
summary: API alive message
description: Returns the current configuration as JSON.
tags:
- Core
responses:
200:
description: Current configuration
schema:
$ref: '#/definitions/ApiConfig'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/discovery_info:
get:
summary: Basic information about Home Assistant instance
tags:
- Core
responses:
200:
description: Basic information
schema:
$ref: '#/definitions/DiscoveryInfo'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/bootstrap:
get:
summary: Returns all data needed to bootstrap Home Assistant.
tags:
- Core
responses:
200:
description: Bootstrap information
schema:
$ref: '#/definitions/BootstrapInfo'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/events:
get:
summary: Array of event objects.
description: Returns an array of event objects. Each event object contain event name and listener count.
tags:
- Events
responses:
200:
description: Events
schema:
type: array
items:
$ref: '#/definitions/Event'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/services:
get:
summary: Array of service objects.
description: Returns an array of service objects. Each object contains the domain and which services it contains.
tags:
- Services
responses:
200:
description: Services
schema:
type: array
items:
$ref: '#/definitions/Service'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/history:
get:
summary: Array of state changes in the past.
description: Returns an array of state changes in the past. Each object contains further detail for the entities.
tags:
- State
responses:
200:
description: State changes
schema:
type: array
items:
$ref: '#/definitions/History'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/states:
get:
summary: Array of state objects.
description: |
Returns an array of state objects. Each state has the following attributes: entity_id, state, last_changed and attributes.
tags:
- State
responses:
200:
description: States
schema:
type: array
items:
$ref: '#/definitions/State'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/states/{entity_id}:
get:
summary: Specific state object.
description: |
Returns a state object for specified entity_id.
tags:
- State
parameters:
- name: entity_id
in: path
description: entity_id of the entity to query
required: true
type: string
responses:
200:
description: State
schema:
$ref: '#/definitions/State'
404:
description: Not found
schema:
$ref: '#/definitions/Message'
default:
description: Error
schema:
$ref: '#/definitions/Message'
post:
description: |
Updates or creates the current state of an entity.
tags:
- State
consumes:
- application/json
parameters:
- name: entity_id
in: path
description: entity_id to set the state of
required: true
type: string
- $ref: '#/parameters/State'
responses:
200:
description: State of existing entity was set
schema:
$ref: '#/definitions/State'
201:
description: State of new entity was set
schema:
$ref: '#/definitions/State'
headers:
location:
type: string
description: location of the new entity
default:
description: Error
schema:
$ref: '#/definitions/Message'
/error_log:
get:
summary: Error log
description: |
Retrieve all errors logged during the current session of Home Assistant as a plaintext response.
tags:
- Core
produces:
- text/plain
responses:
200:
description: Plain text error log
default:
description: Error
schema:
$ref: '#/definitions/Message'
/camera_proxy/camera.{entity_id}:
get:
summary: Camera image.
description: |
Returns the data (image) from the specified camera entity_id.
tags:
- Camera
produces:
- image/jpeg
parameters:
- name: entity_id
in: path
description: entity_id of the camera to query
required: true
type: string
responses:
200:
description: Camera image
schema:
type: file
default:
description: Error
schema:
$ref: '#/definitions/Message'
/events/{event_type}:
post:
description: |
Fires an event with event_type
tags:
- Events
consumes:
- application/json
parameters:
- name: event_type
in: path
description: event_type to fire event with
required: true
type: string
- $ref: '#/parameters/EventData'
responses:
200:
description: Response message
schema:
$ref: '#/definitions/Message'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/services/{domain}/{service}:
post:
description: |
Calls a service within a specific domain. Will return when the service has been executed or 10 seconds has past, whichever comes first.
tags:
- Services
consumes:
- application/json
parameters:
- name: domain
in: path
description: domain of the service
required: true
type: string
- name: service
in: path
description: service to call
required: true
type: string
- $ref: '#/parameters/ServiceData'
responses:
200:
description: List of states that have changed while the service was being executed. The result will include any changed states that changed while the service was being executed, even if their change was the result of something else happening in the system.
schema:
type: array
items:
$ref: '#/definitions/State'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/template:
post:
description: |
Render a Home Assistant template.
tags:
- Template
consumes:
- application/json
produces:
- text/plain
parameters:
- $ref: '#/parameters/Template'
responses:
200:
description: Returns the rendered template in plain text.
schema:
type: string
default:
description: Error
schema:
$ref: '#/definitions/Message'
/event_forwarding:
post:
description: |
Setup event forwarding to another Home Assistant instance.
tags:
- Core
consumes:
- application/json
parameters:
- $ref: '#/parameters/EventForwarding'
responses:
200:
description: It will return a message if event forwarding was setup successful.
schema:
$ref: '#/definitions/Message'
default:
description: Error
schema:
$ref: '#/definitions/Message'
delete:
description: |
Cancel event forwarding to another Home Assistant instance.
tags:
- Core
consumes:
- application/json
parameters:
- $ref: '#/parameters/EventForwarding'
responses:
200:
description: It will return a message if event forwarding was cancelled successful.
schema:
$ref: '#/definitions/Message'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/stream:
get:
summary: Server-sent events
description: The server-sent events feature is a one-way channel from your Home Assistant server to a client which is acting as a consumer.
tags:
- Core
- Events
produces:
- text/event-stream
parameters:
- name: restrict
in: query
description: comma-separated list of event_types to filter
required: false
type: string
responses:
default:
description: Stream of events
schema:
type: object
x-events:
state_changed:
type: object
properties:
entity_id:
type: string
old_state:
$ref: '#/definitions/State'
new_state:
$ref: '#/definitions/State'
definitions:
ApiConfig:
type: object
properties:
components:
type: array
description: List of component types
items:
type: string
description: Component type
latitude:
type: number
format: float
description: Latitude of Home Assistant server
longitude:
type: number
format: float
description: Longitude of Home Assistant server
location_name:
type: string
temperature_unit:
type: string
time_zone:
type: string
version:
type: string
DiscoveryInfo:
type: object
properties:
base_url:
type: string
location_name:
type: string
requires_api_password:
type: boolean
version:
type: string
BootstrapInfo:
type: object
properties:
config:
$ref: '#/definitions/ApiConfig'
events:
type: array
items:
$ref: '#/definitions/Event'
services:
type: array
items:
$ref: '#/definitions/Service'
states:
type: array
items:
$ref: '#/definitions/State'
Event:
type: object
properties:
event:
type: string
listener_count:
type: integer
Service:
type: object
properties:
domain:
type: string
services:
type: object
additionalProperties:
$ref: '#/definitions/DomainService'
DomainService:
type: object
properties:
description:
type: string
fields:
type: object
description: Object with service fields that can be called
State:
type: object
properties:
attributes:
$ref: '#/definitions/StateAttributes'
state:
type: string
entity_id:
type: string
last_changed:
type: string
format: date-time
StateAttributes:
type: object
additionalProperties:
type: string
History:
allOf:
- $ref: '#/definitions/State'
- type: object
properties:
last_updated:
type: string
format: date-time
Message:
type: object
properties:
message:
type: string
parameters:
State:
name: body
in: body
description: State parameter
required: false
schema:
type: object
required:
- state
properties:
attributes:
$ref: '#/definitions/StateAttributes'
state:
type: string
EventData:
name: body
in: body
description: event_data
required: false
schema:
type: object
ServiceData:
name: body
in: body
description: service_data
required: false
schema:
type: object
Template:
name: body
in: body
description: Template to render
required: true
schema:
type: object
required:
- template
properties:
template:
description: Jinja2 template string
type: string
EventForwarding:
name: body
in: body
description: Event Forwarding parameter
required: true
schema:
type: object
required:
- host
- api_password
properties:
host:
type: string
api_password:
type: string
port:
type: integer
-1
View File
@@ -304,7 +304,6 @@ def setup_and_run_hass(config_dir, args):
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
print('Starting Home-Assistant')
hass.start()
exit_code = int(hass.block_till_stopped())
@@ -9,7 +9,6 @@ import os
import voluptuous as vol
from homeassistant.components import verisure
from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY)
@@ -24,11 +23,6 @@ SCAN_INTERVAL = 30
ENTITY_ID_FORMAT = DOMAIN + '.{}'
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
verisure.DISCOVER_ALARMS: 'verisure'
}
SERVICE_TO_METHOD = {
SERVICE_ALARM_DISARM: 'alarm_disarm',
SERVICE_ALARM_ARM_HOME: 'alarm_arm_home',
@@ -50,8 +44,7 @@ ALARM_SERVICE_SCHEMA = vol.Schema({
def setup(hass, config):
"""Track states and offer events for sensors."""
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS)
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
component.setup(config)
+66 -58
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)
if action is not None:
action.run(response.variables)
# 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'])
return self.json(response)
class SpeechType(enum.Enum):
+281 -288
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):
@@ -9,8 +9,6 @@ import logging
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity
from homeassistant.const import (STATE_ON, STATE_OFF)
from homeassistant.components import (
bloomsky, mysensors, zwave, vera, wemo, wink)
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
DOMAIN = 'binary_sensor'
@@ -35,22 +33,11 @@ SENSOR_CLASSES = [
'vibration', # On means vibration detected, Off means no vibration
]
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
bloomsky.DISCOVER_BINARY_SENSORS: 'bloomsky',
mysensors.DISCOVER_BINARY_SENSORS: 'mysensors',
zwave.DISCOVER_BINARY_SENSORS: 'zwave',
vera.DISCOVER_BINARY_SENSORS: 'vera',
wemo.DISCOVER_BINARY_SENSORS: 'wemo',
wink.DISCOVER_BINARY_SENSORS: 'wink'
}
def setup(hass, config):
"""Track states and offer events for binary sensors."""
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS)
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
component.setup(config)
@@ -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})
@@ -35,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
rest.update()
if rest.data is None:
_LOGGER.error('Unable to fetch Rest data')
_LOGGER.error('Unable to fetch REST data')
return False
add_devices([RestBinarySensor(
@@ -57,6 +57,7 @@ class RestBinarySensor(BinarySensorDevice):
self._name = name
self._sensor_class = sensor_class
self._state = False
self._previous_data = None
self._value_template = value_template
self.update()
@@ -77,9 +78,14 @@ class RestBinarySensor(BinarySensorDevice):
return False
if self._value_template is not None:
self.rest.data = template.render_with_possible_json_value(
response = template.render_with_possible_json_value(
self._hass, self._value_template, self.rest.data, False)
return bool(int(self.rest.data))
try:
return bool(int(response))
except ValueError:
return {"true": True, "on": True, "open": True,
"yes": True}.get(response.lower(), False)
def update(self):
"""Get the latest data from REST API and updates the state."""
@@ -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):
@@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['python-wink==0.7.6']
REQUIREMENTS = ['python-wink==0.7.7']
# These are the available sensors mapped to binary_sensor class
SENSOR_TYPES = {
+3 -11
View File
@@ -9,9 +9,8 @@ from datetime import timedelta
import requests
from homeassistant.components import discovery
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import validate_config
from homeassistant.helpers import validate_config, discovery
from homeassistant.util import Throttle
DOMAIN = "bloomsky"
@@ -23,10 +22,6 @@ _LOGGER = logging.getLogger(__name__)
# no point in polling the API more frequently
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
DISCOVER_SENSORS = 'bloomsky.sensors'
DISCOVER_BINARY_SENSORS = 'bloomsky.binary_sensor'
DISCOVER_CAMERAS = 'bloomsky.camera'
# pylint: disable=unused-argument,too-few-public-methods
def setup(hass, config):
@@ -45,11 +40,8 @@ def setup(hass, config):
except RuntimeError:
return False
for component, discovery_service in (
('camera', DISCOVER_CAMERAS), ('sensor', DISCOVER_SENSORS),
('binary_sensor', DISCOVER_BINARY_SENSORS)):
discovery.discover(hass, discovery_service, component=component,
hass_config=config)
for component in 'camera', 'binary_sensor', 'sensor':
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True
+95 -89
View File
@@ -6,96 +6,35 @@ 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']
SCAN_INTERVAL = 30
ENTITY_ID_FORMAT = DOMAIN + '.{}'
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
bloomsky.DISCOVER_CAMERAS: 'bloomsky',
}
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
def setup(hass, config):
"""Setup the camera component."""
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS)
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
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 +45,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 +58,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 +79,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
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)
return response(
stream(),
content_type=('multipart/x-mixed-replace; '
'boundary=--jpegboundary')
)
@property
def state(self):
@@ -175,7 +122,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 +133,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)
+1 -1
View File
@@ -49,7 +49,7 @@ class FoscamCamera(Camera):
def camera_image(self):
"""Return a still image reponse from the camera."""
# Send the request to snap a picture and return raw jpg data
response = requests.get(self._snap_picture_url)
response = requests.get(self._snap_picture_url, timeout=10)
return response.content
+3 -2
View File
@@ -43,13 +43,14 @@ class GenericCamera(Camera):
try:
response = requests.get(
self._still_image_url,
auth=HTTPBasicAuth(self._username, self._password))
auth=HTTPBasicAuth(self._username, self._password),
timeout=10)
except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error)
return None
else:
try:
response = requests.get(self._still_image_url)
response = requests.get(self._still_image_url, timeout=10)
except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error)
return None
@@ -0,0 +1,53 @@
"""Camera that loads a picture from a local file."""
import logging
import os
from homeassistant.components.camera import Camera
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Camera."""
# check for missing required configuration variable
if config.get("file_path") is None:
_LOGGER.error("Missing required variable: file_path")
return False
setup_config = (
{
"name": config.get("name", "Local File"),
"file_path": config.get("file_path")
}
)
# check filepath given is readable
if not os.access(setup_config["file_path"], os.R_OK):
_LOGGER.error("file path is not readable")
return False
add_devices([
LocalFile(setup_config)
])
class LocalFile(Camera):
"""Local camera."""
def __init__(self, device_info):
"""Initialize Local File Camera component."""
super().__init__()
self._name = device_info["name"]
self._config = device_info
def camera_image(self):
"""Return image response."""
with open(self._config["file_path"], 'rb') as file:
return file.read()
@property
def name(self):
"""Return the name of this camera."""
return self._name
+9 -16
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'
@@ -47,10 +46,9 @@ class MjpegCamera(Camera):
return requests.get(self._mjpeg_url,
auth=HTTPBasicAuth(self._username,
self._password),
stream=True)
stream=True, timeout=10)
else:
return requests.get(self._mjpeg_url,
stream=True)
return requests.get(self._mjpeg_url, stream=True, timeout=10)
def camera_image(self):
"""Return a still image response from the camera."""
@@ -68,19 +66,14 @@ 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()
return response(
stream.iter_content(chunk_size=1024),
mimetype=stream.headers[CONTENT_TYPE_HEADER],
direct_passthrough=True
)
@property
def name(self):
+104
View File
@@ -0,0 +1,104 @@
"""
Support for the Netatmo Welcome camera.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.netatmo/
"""
import logging
from datetime import timedelta
import requests
from homeassistant.util import Throttle
from homeassistant.components.camera import Camera
from homeassistant.loader import get_component
DEPENDENCIES = ["netatmo"]
_LOGGER = logging.getLogger(__name__)
CONF_HOME = 'home'
ATTR_CAMERAS = 'cameras'
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup access to Netatmo Welcome cameras."""
netatmo = get_component('netatmo')
home = config.get(CONF_HOME, None)
data = WelcomeData(netatmo.NETATMO_AUTH, home)
for camera_name in data.get_camera_names():
if ATTR_CAMERAS in config:
if camera_name not in config[ATTR_CAMERAS]:
continue
add_devices_callback([WelcomeCamera(data, camera_name, home)])
class WelcomeCamera(Camera):
"""Representation of the images published from Welcome camera."""
def __init__(self, data, camera_name, home):
"""Setup for access to the BloomSky camera images."""
super(WelcomeCamera, self).__init__()
self._data = data
self._camera_name = camera_name
if home:
self._name = home + ' / ' + camera_name
else:
self._name = camera_name
self._vpnurl, self._localurl = self._data.welcomedata.cameraUrls(
camera=camera_name
)
def camera_image(self):
"""Return a still image response from the camera."""
try:
if self._localurl:
response = requests.get('{0}/live/snapshot_720.jpg'.format(
self._localurl), timeout=10)
else:
response = requests.get('{0}/live/snapshot_720.jpg'.format(
self._vpnurl), timeout=10)
except requests.exceptions.RequestException as error:
_LOGGER.error('Welcome VPN url changed: %s', error)
self._data.update()
(self._vpnurl, self._localurl) = \
self._data.welcomedata.cameraUrls(camera=self._camera_name)
return None
return response.content
@property
def name(self):
"""Return the name of this Netatmo Welcome device."""
return self._name
class WelcomeData(object):
"""Get the latest data from NetAtmo."""
def __init__(self, auth, home=None):
"""Initialize the data object."""
self.auth = auth
self.welcomedata = None
self.camera_names = []
self.home = home
def get_camera_names(self):
"""Return all module available on the API as a list."""
self.update()
if not self.home:
for home in self.welcomedata.cameras.keys():
for camera in self.welcomedata.cameras[home].values():
self.camera_names.append(camera['name'])
else:
for camera in self.welcomedata.cameras[self.home].values():
self.camera_names.append(camera['name'])
return self.camera_names
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Call the NetAtmo API to update the data."""
import lnetatmo
self.welcomedata = lnetatmo.WelcomeData(self.auth)
+14 -7
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))
+2 -1
View File
@@ -8,7 +8,7 @@ the user has submitted configuration information.
"""
import logging
from homeassistant.const import EVENT_TIME_CHANGED
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME
from homeassistant.helpers.entity import generate_entity_id
DOMAIN = "configurator"
@@ -118,6 +118,7 @@ class Configurator(object):
data = {
ATTR_CONFIGURE_ID: request_id,
ATTR_FIELDS: fields,
ATTR_FRIENDLY_NAME: name,
}
data.update({
+1 -1
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):
+14 -1
View File
@@ -67,7 +67,9 @@ def setup(hass, config):
lights[1], switches[0], 'input_select.living_room_preset',
'rollershutter.living_room_window', media_players[1],
'scene.romantic_lights'])
group.Group(hass, 'bedroom', [lights[0], switches[1], media_players[0]])
group.Group(hass, 'bedroom', [
lights[0], switches[1], media_players[0],
'input_slider.noise_allowance'])
group.Group(hass, 'kitchen', [
lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door'])
group.Group(hass, 'doors', [
@@ -145,6 +147,17 @@ def setup(hass, config):
{'input_boolean': {'notify': {'icon': 'mdi:car',
'initial': False,
'name': 'Notify Anne Therese is home'}}})
# Set up input boolean
bootstrap.setup_component(
hass, 'input_slider',
{'input_slider': {
'noise_allowance': {'icon': 'mdi:bell-ring',
'min': 0,
'max': 10,
'name': 'Allowed Noise',
'unit_of_measurement': 'dB'}}})
# Set up weblink
bootstrap.setup_component(
hass, 'weblink',
@@ -12,12 +12,13 @@ import os
import threading
from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.components import discovery, group, zone
from homeassistant.components import group, zone
from homeassistant.components.discovery import SERVICE_NETGEAR
from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform
from homeassistant.helpers import config_per_platform, discovery
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 +27,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']
@@ -61,7 +63,7 @@ ATTR_GPS = 'gps'
ATTR_BATTERY = 'battery'
DISCOVERY_PLATFORMS = {
discovery.SERVICE_NETGEAR: 'netgear',
SERVICE_NETGEAR: 'netgear',
}
_LOGGER = logging.getLogger(__name__)
@@ -94,8 +96,11 @@ def setup(hass, config):
yaml_path = hass.config.path(YAML_DEVICES)
conf = config.get(DOMAIN, {})
if isinstance(conf, list) and len(conf) > 0:
conf = conf[0]
# Config can be an empty list. In that case, substitute a dict
if isinstance(conf, list):
conf = conf[0] if len(conf) > 0 else {}
consider_home = timedelta(
seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int,
DEFAULT_CONSIDER_HOME))
@@ -193,7 +198,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:
@@ -6,8 +6,10 @@ https://home-assistant.io/components/device_tracker.asuswrt/
"""
import logging
import re
import socket
import telnetlib
import threading
from collections import namedtuple
from datetime import timedelta
from homeassistant.components.device_tracker import DOMAIN
@@ -28,6 +30,21 @@ _LEASES_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
r'(?P<host>([^\s]+))')
# command to get both 5GHz and 2.4GHz clients
_WL_CMD = '{ wl -i eth2 assoclist & wl -i eth1 assoclist ; }'
_WL_REGEX = re.compile(
r'\w+\s' +
r'(?P<mac>(([0-9A-F]{2}[:-]){5}([0-9A-F]{2})))')
_ARP_CMD = 'arp -n'
_ARP_REGEX = re.compile(
r'.+\s' +
r'\((?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\)\s' +
r'.+\s' +
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))' +
r'\s' +
r'.*')
_IP_NEIGH_CMD = 'ip neigh'
_IP_NEIGH_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
@@ -41,24 +58,35 @@ _IP_NEIGH_REGEX = re.compile(
def get_scanner(hass, config):
"""Validate the configuration and return an ASUS-WRT scanner."""
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
{DOMAIN: [CONF_HOST, CONF_USERNAME]},
_LOGGER):
return None
elif CONF_PASSWORD not in config[DOMAIN] and \
'pub_key' not in config[DOMAIN]:
_LOGGER.error("Either a public key or password must be provided")
return None
scanner = AsusWrtDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp')
class AsusWrtDeviceScanner(object):
"""This class queries a router running ASUSWRT firmware."""
# pylint: disable=too-many-instance-attributes, too-many-branches
# Eighth attribute needed for mode (AP mode vs router mode)
def __init__(self, config):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.username = str(config[CONF_USERNAME])
self.password = str(config[CONF_PASSWORD])
self.password = str(config.get(CONF_PASSWORD))
self.pub_key = str(config.get('pub_key'))
self.protocol = config.get('protocol')
self.mode = config.get('mode')
self.lock = threading.Lock()
@@ -106,21 +134,40 @@ class AsusWrtDeviceScanner(object):
def ssh_connection(self):
"""Retrieve data from ASUSWRT via the ssh protocol."""
from pexpect import pxssh
from pexpect import pxssh, exceptions
try:
ssh = pxssh.pxssh()
ssh.login(self.host, self.username, self.password)
if self.pub_key:
ssh.login(self.host, self.username, ssh_key=self.pub_key)
elif self.password:
ssh.login(self.host, self.username, self.password)
else:
_LOGGER.error('No password or public key specified')
return None
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]
if self.mode == 'ap':
ssh.sendline(_ARP_CMD)
ssh.prompt()
arp_result = ssh.before.split(b'\n')[1:-1]
ssh.sendline(_WL_CMD)
ssh.prompt()
leases_result = ssh.before.split(b'\n')[1:-1]
else:
arp_result = ['']
ssh.sendline(_LEASES_CMD)
ssh.prompt()
leases_result = ssh.before.split(b'\n')[1:-1]
ssh.logout()
return (neighbors, leases_result)
return AsusWrtResult(neighbors, leases_result, arp_result)
except pxssh.ExceptionPxssh as exc:
_LOGGER.exception('Unexpected response from router: %s', exc)
return ('', '')
_LOGGER.error('Unexpected response from router: %s', exc)
return None
except exceptions.EOF:
_LOGGER.error('Connection refused or no route to host')
return None
def telnet_connection(self):
"""Retrieve data from ASUSWRT via the telnet protocol."""
@@ -133,47 +180,99 @@ class AsusWrtDeviceScanner(object):
prompt_string = telnet.read_until(b'#').split(b'\n')[-1]
telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii'))
neighbors = telnet.read_until(prompt_string).split(b'\n')[1:-1]
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
leases_result = telnet.read_until(prompt_string).split(b'\n')[1:-1]
if self.mode == 'ap':
telnet.write('{}\n'.format(_ARP_CMD).encode('ascii'))
arp_result = (telnet.read_until(prompt_string).
split(b'\n')[1:-1])
telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
leases_result = (telnet.read_until(prompt_string).
split(b'\n')[1:-1])
else:
arp_result = ['']
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)
return AsusWrtResult(neighbors, leases_result, arp_result)
except EOFError:
_LOGGER.exception("Unexpected response from router")
return ('', '')
_LOGGER.error("Unexpected response from router")
return None
except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router,"
" is telnet enabled?")
return ('', '')
_LOGGER.error("Connection refused by router, is telnet enabled?")
return None
except socket.gaierror as exc:
_LOGGER.error("Socket exception: %s", exc)
return None
except OSError as exc:
_LOGGER.error("OSError: %s", exc)
return None
def get_asuswrt_data(self):
"""Retrieve data from ASUSWRT and return parsed result."""
if self.protocol == 'telnet':
neighbors, leases_result = self.telnet_connection()
if self.protocol == 'ssh':
result = self.ssh_connection()
elif self.protocol == 'telnet':
result = self.telnet_connection()
else:
neighbors, leases_result = self.ssh_connection()
# autodetect protocol
result = self.ssh_connection()
if result:
self.protocol = 'ssh'
else:
result = self.telnet_connection()
if result:
self.protocol = 'telnet'
if not result:
return {}
devices = {}
for lease in leases_result:
match = _LEASES_REGEX.search(lease.decode('utf-8'))
if self.mode == 'ap':
for lease in result.leases:
match = _WL_REGEX.search(lease.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse lease row: %s", lease)
continue
if not match:
_LOGGER.warning("Could not parse wl row: %s", lease)
continue
# For leases where the client doesn't set a hostname, ensure it is
# blank and not '*', which breaks the entity_id down the line.
host = match.group('host')
if host == '*':
host = ''
devices[match.group('ip')] = {
'host': host,
'status': '',
'ip': match.group('ip'),
'mac': match.group('mac').upper(),
}
# match mac addresses to IP addresses in ARP table
for arp in result.arp:
if match.group('mac').lower() in arp.decode('utf-8'):
arp_match = _ARP_REGEX.search(arp.decode('utf-8'))
if not arp_match:
_LOGGER.warning("Could not parse arp row: %s", arp)
continue
for neighbor in neighbors:
devices[arp_match.group('ip')] = {
'host': host,
'status': '',
'ip': arp_match.group('ip'),
'mac': match.group('mac').upper(),
}
else:
for lease in result.leases:
match = _LEASES_REGEX.search(lease.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse lease row: %s", lease)
continue
# For leases where the client doesn't set a hostname, ensure it
# is blank and not '*', which breaks entity_id down the line.
host = match.group('host')
if host == '*':
host = ''
devices[match.group('ip')] = {
'host': host,
'status': '',
'ip': match.group('ip'),
'mac': match.group('mac').upper(),
}
for neighbor in result.neighbors:
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse neighbor row: %s", neighbor)
@@ -0,0 +1,141 @@
"""
Support for BT Home Hub 5.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.bt_home_hub_5/
"""
import logging
import re
import threading
from datetime import timedelta
import xml.etree.ElementTree as ET
import json
from urllib.parse import unquote
import requests
from homeassistant.helpers import validate_config
from homeassistant.components.device_tracker import DOMAIN
from homeassistant.const import CONF_HOST
from homeassistant.util import Throttle
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
_LOGGER = logging.getLogger(__name__)
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Return a BT Home Hub 5 scanner if successful."""
if not validate_config(config,
{DOMAIN: [CONF_HOST]},
_LOGGER):
return None
scanner = BTHomeHub5DeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class BTHomeHub5DeviceScanner(object):
"""This class queries a BT Home Hub 5."""
def __init__(self, config):
"""Initialise the scanner."""
_LOGGER.info("Initialising BT Home Hub 5")
self.host = config.get(CONF_HOST, '192.168.1.254')
self.lock = threading.Lock()
self.last_results = {}
self.url = 'http://{}/nonAuth/home_status.xml'.format(self.host)
# Test the router is accessible
data = _get_homehub_data(self.url)
self.success_init = data is not None
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return (device for device in self.last_results)
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
with self.lock:
# If not initialised and not already scanned and not found.
if device not in self.last_results:
self._update_info()
if not self.last_results:
return None
return self.last_results.get(device)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the BT Home Hub 5 is up to date.
Return boolean if scanning successful.
"""
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Scanning")
data = _get_homehub_data(self.url)
if not data:
_LOGGER.warning('Error scanning devices')
return False
self.last_results = data
return True
def _get_homehub_data(url):
"""Retrieve data from BT Home Hub 5 and return parsed result."""
try:
response = requests.get(url, timeout=5)
except requests.exceptions.Timeout:
_LOGGER.exception("Connection to the router timed out")
return
if response.status_code == 200:
return _parse_homehub_response(response.text)
else:
_LOGGER.error("Invalid response from Home Hub: %s", response)
def _parse_homehub_response(data_str):
"""Parse the BT Home Hub 5 data format."""
root = ET.fromstring(data_str)
dirty_json = root.find('known_device_list').get('value')
# Normalise the JavaScript data to JSON.
clean_json = unquote(dirty_json.replace('\'', '\"')
.replace('{', '{\"')
.replace(':\"', '\":\"')
.replace('\",', '\",\"'))
known_devices = [x for x in json.loads(clean_json) if x]
devices = {}
for device in known_devices:
name = device.get('name')
mac = device.get('mac')
if _MAC_REGEX.match(mac) or ',' in mac:
for mac_addr in mac.split(','):
if _MAC_REGEX.match(mac_addr):
devices[mac_addr] = name
else:
devices[mac] = name
return devices
@@ -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:locative"
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)
@@ -186,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,
@@ -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:
+18 -90
View File
@@ -9,100 +9,30 @@ loaded before the EVENT_PLATFORM_DISCOVERED is fired.
import logging
import threading
from homeassistant import bootstrap
from homeassistant.const import (
ATTR_DISCOVERED, ATTR_SERVICE, EVENT_HOMEASSISTANT_START,
EVENT_PLATFORM_DISCOVERED)
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.helpers.discovery import load_platform, discover
DOMAIN = "discovery"
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'
SERVICE_NETGEAR = 'netgear_router'
SERVICE_SONOS = 'sonos'
SERVICE_PLEX = 'plex_mediaserver'
SERVICE_SQUEEZEBOX = 'logitech_mediaserver'
SERVICE_PANASONIC_VIERA = 'panasonic_viera'
SERVICE_ROKU = 'roku'
SERVICE_HANDLERS = {
SERVICE_WEMO: "wemo",
SERVICE_CAST: "media_player",
SERVICE_HUE: "light",
SERVICE_NETGEAR: 'device_tracker',
SERVICE_SONOS: 'media_player',
SERVICE_PLEX: 'media_player',
SERVICE_SQUEEZEBOX: 'media_player',
SERVICE_PANASONIC_VIERA: 'media_player',
SERVICE_ROKU: 'media_player',
SERVICE_NETGEAR: ('device_tracker', None),
SERVICE_WEMO: ('wemo', None),
'philips_hue': ('light', 'hue'),
'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'),
'plex_mediaserver': ('media_player', 'plex'),
'roku': ('media_player', 'roku'),
'sonos': ('media_player', 'sonos'),
'logitech_mediaserver': ('media_player', 'squeezebox'),
}
def listen(hass, service, callback):
"""Setup listener for discovery of specific service.
Service can be a string or a list/tuple.
"""
if isinstance(service, str):
service = (service,)
else:
service = tuple(service)
def discovery_event_listener(event):
"""Listen for discovery events."""
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)
def discover(hass, service, discovered=None, component=None, hass_config=None):
"""Fire discovery event. Can ensure a component is loaded."""
if component is not None:
bootstrap.setup_component(hass, component, hass_config)
data = {
ATTR_SERVICE: service
}
if discovered is not None:
data[ATTR_DISCOVERED] = discovered
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__)
@@ -119,20 +49,18 @@ def setup(hass, config):
with lock:
logger.info("Found new service: %s %s", service, info)
component = SERVICE_HANDLERS.get(service)
comp_plat = SERVICE_HANDLERS.get(service)
# We do not know how to handle this service.
if not component:
if not comp_plat:
return
# This component cannot be setup.
if not bootstrap.setup_component(hass, component, config):
return
component, platform = comp_plat
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: service,
ATTR_DISCOVERED: info
})
if platform is None:
discover(hass, service, info, component, config)
else:
load_platform(hass, component, platform, info, config)
# pylint: disable=unused-argument
def start_discovery(event):
+6 -21
View File
@@ -8,21 +8,18 @@ import logging
import os
from datetime import timedelta
from homeassistant import bootstrap
from homeassistant.const import (
ATTR_DISCOVERED, ATTR_SERVICE, CONF_API_KEY, EVENT_PLATFORM_DISCOVERED)
from homeassistant.helpers import discovery
from homeassistant.const import CONF_API_KEY
from homeassistant.loader import get_component
from homeassistant.util import Throttle
DOMAIN = "ecobee"
DISCOVER_THERMOSTAT = "ecobee.thermostat"
DISCOVER_SENSORS = "ecobee.sensor"
NETWORK = None
HOLD_TEMP = 'hold_temp'
REQUIREMENTS = [
'https://github.com/nkgilley/python-ecobee-api/archive/'
'4a884bc146a93991b4210f868f3d6aecf0a181e6.zip#python-ecobee==0.0.5']
'4856a704670c53afe1882178a89c209b5f98533d.zip#python-ecobee==0.0.6']
_LOGGER = logging.getLogger(__name__)
@@ -70,23 +67,11 @@ def setup_ecobee(hass, network, config):
configurator = get_component('configurator')
configurator.request_done(_CONFIGURING.pop('ecobee'))
# Ensure component is loaded
bootstrap.setup_component(hass, 'thermostat', config)
bootstrap.setup_component(hass, 'sensor', config)
hold_temp = config[DOMAIN].get(HOLD_TEMP, False)
# Fire thermostat discovery event
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: DISCOVER_THERMOSTAT,
ATTR_DISCOVERED: {'hold_temp': hold_temp}
})
# Fire sensor discovery event
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: DISCOVER_SENSORS,
ATTR_DISCOVERED: {}
})
discovery.load_platform(hass, 'thermostat', DOMAIN,
{'hold_temp': hold_temp}, config)
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
# pylint: disable=too-few-public-methods
+117
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)
+70 -6
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
+76 -96
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')
@@ -1,2 +1,2 @@
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
VERSION = "1baebe8155deb447230866d7ae854bd9"
VERSION = "9ee3d4466a65bef35c2c8974e91b37c0"
@@ -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,11 @@
<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/home-assistant-polymer/build/_demo_data_compiled.js'></script> #}
<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 +92,5 @@
document.head.appendChild(script)
}
</script>
<home-assistant auth='{{ auth }}' icons='{{ icons }}'></home-assistant>
</body>
</html>
+2 -1
View File
@@ -1,2 +1,3 @@
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
VERSION = "0a226e905af198b2dabf1ce154844568"
CORE = "7962327e4a29e51d4a6f4ee6cca9acc3"
UI = "570e1b8744a58024fc4e256f5e024424"
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because one or more lines are too long
@@ -4,16 +4,27 @@
"start_url": "/",
"display": "standalone",
"theme_color": "#03A9F4",
"background_color": "#FFFFFF",
"icons": [
{
"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"
},
{
"src": "/static/favicon-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/static/favicon-1024x1024.png",
"sizes": "1024x1024",
"type": "image/png"
}
]
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
User-agent: *
Disallow: /
@@ -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=194)}({194: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 = [["/","595e12c9755af231fd80191e4cc74d2e"],["/devEvent","595e12c9755af231fd80191e4cc74d2e"],["/devInfo","595e12c9755af231fd80191e4cc74d2e"],["/devService","595e12c9755af231fd80191e4cc74d2e"],["/devState","595e12c9755af231fd80191e4cc74d2e"],["/devTemplate","595e12c9755af231fd80191e4cc74d2e"],["/history","595e12c9755af231fd80191e4cc74d2e"],["/logbook","595e12c9755af231fd80191e4cc74d2e"],["/map","595e12c9755af231fd80191e4cc74d2e"],["/states","595e12c9755af231fd80191e4cc74d2e"],["/static/core-7962327e4a29e51d4a6f4ee6cca9acc3.js","9c07ffb3f81cfb74f8a051b80cc8f9f0"],["/static/frontend-570e1b8744a58024fc4e256f5e024424.html","595e12c9755af231fd80191e4cc74d2e"],["/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

@@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN, SERVICE_CLOSE, SERVICE_OPEN,
ATTR_ENTITY_ID)
from homeassistant.components import (group, wink)
from homeassistant.components import group
DOMAIN = 'garage_door'
SCAN_INTERVAL = 30
@@ -27,11 +27,6 @@ ENTITY_ID_ALL_GARAGE_DOORS = group.ENTITY_ID_FORMAT.format('all_garage_doors')
ENTITY_ID_FORMAT = DOMAIN + '.{}'
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
wink.DISCOVER_GARAGE_DOORS: 'wink'
}
GARAGE_DOOR_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
@@ -60,8 +55,7 @@ def open_door(hass, entity_id=None):
def setup(hass, config):
"""Track states and offer events for garage door."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
GROUP_NAME_ALL_GARAGE_DOORS)
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_GARAGE_DOORS)
component.setup(config)
def handle_garage_door_service(service):
+1 -1
View File
@@ -9,7 +9,7 @@ import logging
from homeassistant.components.garage_door import GarageDoorDevice
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
REQUIREMENTS = ['python-wink==0.7.6']
REQUIREMENTS = ['python-wink==0.7.7']
def setup_platform(hass, config, add_devices, discovery_info=None):
+1 -1
View File
@@ -39,7 +39,7 @@ def _conf_preprocess(value):
return value
_SINGLE_GROUP_CONFIG = vol.Schema(vol.All(_conf_preprocess, {
vol.Optional(CONF_ENTITIES): vol.Any(None, cv.entity_ids),
vol.Optional(CONF_ENTITIES): vol.Any(cv.entity_ids, None),
CONF_VIEW: bool,
CONF_NAME: str,
CONF_ICON: cv.icon,
+27 -32
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):
+350 -419
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,342 @@ 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
return self.Response(wrap_file(request.environ, fil),
mimetype=mimetype, direct_passthrough=True)
# 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
def options(self, request):
"""Default handler for OPTIONS (necessary for CORS preflight)."""
return self.Response('', status=200)
+3 -9
View File
@@ -14,7 +14,6 @@ import homeassistant.util as util
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.temperature import convert
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components import zwave
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
TEMP_CELCIUS)
@@ -57,10 +56,6 @@ ATTR_SWING_LIST = "swing_list"
_LOGGER = logging.getLogger(__name__)
DISCOVERY_PLATFORMS = {
zwave.DISCOVER_HVAC: 'zwave'
}
def set_away_mode(hass, away_mode, entity_id=None):
"""Turn all or specified hvac away mode on."""
@@ -139,8 +134,7 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
# pylint: disable=too-many-branches
def setup(hass, config):
"""Setup hvacs."""
component = EntityComponent(_LOGGER, DOMAIN, hass,
SCAN_INTERVAL, DISCOVERY_PLATFORMS)
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
component.setup(config)
descriptions = load_yaml_config_file(
@@ -468,12 +462,12 @@ class HvacDevice(Entity):
@property
def min_temp(self):
"""Return the minimum temperature."""
return convert(7, TEMP_CELCIUS, self.unit_of_measurement)
return convert(19, TEMP_CELCIUS, self.unit_of_measurement)
@property
def max_temp(self):
"""Return the maximum temperature."""
return convert(35, TEMP_CELCIUS, self.unit_of_measurement)
return convert(30, TEMP_CELCIUS, self.unit_of_measurement)
@property
def min_humidity(self):
-10
View File
@@ -233,13 +233,3 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
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)
+15 -8
View File
@@ -8,7 +8,7 @@ import logging
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.const import ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
@@ -68,18 +68,18 @@ def setup(hass, config):
name = cfg.get(CONF_NAME)
minimum = cfg.get(CONF_MIN)
maximum = cfg.get(CONF_MAX)
state = cfg.get(CONF_INITIAL)
step = cfg.get(CONF_STEP)
state = cfg.get(CONF_INITIAL, minimum)
step = cfg.get(CONF_STEP, 1)
icon = cfg.get(CONF_ICON)
unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT)
if state < minimum:
state = minimum
if state > maximum:
state = maximum
entities.append(
InputSlider(object_id, name, state, minimum, maximum, step, icon)
)
entities.append(InputSlider(object_id, name, state, minimum, maximum,
step, icon, unit))
if not entities:
return False
@@ -103,8 +103,9 @@ def setup(hass, config):
class InputSlider(Entity):
"""Represent an slider."""
# pylint: disable=too-many-arguments
def __init__(self, object_id, name, state, minimum, maximum, step, icon):
# pylint: disable=too-many-arguments, too-many-instance-attributes
def __init__(self, object_id, name, state, minimum, maximum, step, icon,
unit):
"""Initialize a select input."""
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
self._name = name
@@ -113,6 +114,7 @@ class InputSlider(Entity):
self._maximum = maximum
self._step = step
self._icon = icon
self._unit = unit
@property
def should_poll(self):
@@ -134,6 +136,11 @@ class InputSlider(Entity):
"""State of the component."""
return self._current_value
@property
def unit_of_measurement(self):
"""Unit of measurement of slider."""
return self._unit
@property
def state_attributes(self):
"""State attributes."""
+5 -55
View File
@@ -6,18 +6,12 @@ https://home-assistant.io/components/insteon_hub/
"""
import logging
import homeassistant.bootstrap as bootstrap
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
from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import validate_config, discovery
DOMAIN = "insteon_hub"
REQUIREMENTS = ['insteon_hub==0.4.5']
INSTEON = None
DISCOVER_LIGHTS = "insteon_hub.lights"
_LOGGER = logging.getLogger(__name__)
@@ -45,51 +39,7 @@ def setup(hass, config):
_LOGGER.error("Could not connect to Insteon service.")
return
comp_name = 'light'
discovery = DISCOVER_LIGHTS
component = get_component(comp_name)
bootstrap.setup_component(hass, component.DOMAIN, config)
hass.bus.fire(
EVENT_PLATFORM_DISCOVERED,
{ATTR_SERVICE: discovery, ATTR_DISCOVERED: {}})
for component in 'light':
discovery.load_platform(hass, component, DOMAIN, {}, config)
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')
+8 -18
View File
@@ -7,19 +7,15 @@ https://home-assistant.io/components/isy994/
import logging
from urllib.parse import urlparse
from homeassistant import bootstrap
from homeassistant.const import (
ATTR_DISCOVERED, ATTR_SERVICE, CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP, EVENT_PLATFORM_DISCOVERED)
from homeassistant.helpers import validate_config
CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import validate_config, discovery
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.loader import get_component
DOMAIN = "isy994"
REQUIREMENTS = ['PyISY==1.0.5']
DISCOVER_LIGHTS = "isy994.lights"
DISCOVER_SWITCHES = "isy994.switches"
DISCOVER_SENSORS = "isy994.sensors"
REQUIREMENTS = ['PyISY==1.0.6']
ISY = None
SENSOR_STRING = 'Sensor'
HIDDEN_STRING = '{HIDE ME}'
@@ -76,15 +72,9 @@ def setup(hass, config):
# Listen for HA stop to disconnect.
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop)
# Load components for the devices in the ISY controller that we support.
for comp_name, discovery in ((('sensor', DISCOVER_SENSORS),
('light', DISCOVER_LIGHTS),
('switch', DISCOVER_SWITCHES))):
component = get_component(comp_name)
bootstrap.setup_component(hass, component.DOMAIN, config)
hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
{ATTR_SERVICE: discovery,
ATTR_DISCOVERED: {}})
# Load platforms for the devices in the ISY controller that we support.
for component in ('sensor', 'light', 'switch'):
discovery.load_platform(hass, component, DOMAIN, {}, config)
ISY.auto_update = True
return True
+2 -18
View File
@@ -10,9 +10,7 @@ import csv
import voluptuous as vol
from homeassistant.components import (
group, discovery, wemo, wink, isy994,
zwave, insteon_hub, mysensors, tellstick, vera)
from homeassistant.components import group
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
@@ -60,19 +58,6 @@ EFFECT_WHITE = "white"
LIGHT_PROFILES_FILE = "light_profiles.csv"
# Maps discovered services to their platforms.
DISCOVERY_PLATFORMS = {
wemo.DISCOVER_LIGHTS: 'wemo',
wink.DISCOVER_LIGHTS: 'wink',
insteon_hub.DISCOVER_LIGHTS: 'insteon_hub',
isy994.DISCOVER_LIGHTS: 'isy994',
discovery.SERVICE_HUE: 'hue',
zwave.DISCOVER_LIGHTS: 'zwave',
mysensors.DISCOVER_LIGHTS: 'mysensors',
tellstick.DISCOVER_LIGHTS: 'tellstick',
vera.DISCOVER_LIGHTS: 'vera',
}
PROP_TO_ATTR = {
'brightness': ATTR_BRIGHTNESS,
'color_temp': ATTR_COLOR_TEMP,
@@ -172,8 +157,7 @@ def toggle(hass, entity_id=None, transition=None):
def setup(hass, config):
"""Expose light control via statemachine and services."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
GROUP_NAME_ALL_LIGHTS)
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS)
component.setup(config)
# Load built-in profiles and custom profiles
+92
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()
+52 -1
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')
@@ -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)
+1 -1
View File
@@ -13,7 +13,7 @@ from homeassistant.util import color as color_util
from homeassistant.util.color import \
color_temperature_mired_to_kelvin as mired_to_kelvin
REQUIREMENTS = ['python-wink==0.7.6']
REQUIREMENTS = ['python-wink==0.7.7']
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
+85
View File
@@ -0,0 +1,85 @@
"""
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
_LOGGER.debug('LIRC interface thread started')
while not self.stopped.isSet():
try:
code = lirc.nextcode() # list; empty if no buttons pressed
except lirc.NextCodeError:
_LOGGER.warning('Encountered error reading '
'next code from LIRC')
code = None
# 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)
lirc.deinit()
_LOGGER.debug('LIRC interface thread stopped')
+2 -10
View File
@@ -18,7 +18,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED,
STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK)
from homeassistant.components import (group, verisure, wink, zwave)
from homeassistant.components import group
DOMAIN = 'lock'
SCAN_INTERVAL = 30
@@ -30,13 +30,6 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
wink.DISCOVER_LOCKS: 'wink',
verisure.DISCOVER_LOCKS: 'verisure',
zwave.DISCOVER_LOCKS: 'zwave',
}
LOCK_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_CODE): cv.string,
@@ -76,8 +69,7 @@ def unlock(hass, entity_id=None, code=None):
def setup(hass, config):
"""Track states and offer events for locks."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
GROUP_NAME_ALL_LOCKS)
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LOCKS)
component.setup(config)
def handle_lock_service(service):
+1 -1
View File
@@ -9,7 +9,7 @@ import logging
from homeassistant.components.lock import LockDevice
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
REQUIREMENTS = ['python-wink==0.7.6']
REQUIREMENTS = ['python-wink==0.7.7']
def setup_platform(hass, config, add_devices, discovery_info=None):
+20 -19
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):
@@ -9,7 +9,6 @@ import os
import voluptuous as vol
from homeassistant.components import discovery
from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
@@ -30,15 +29,6 @@ SCAN_INTERVAL = 10
ENTITY_ID_FORMAT = DOMAIN + '.{}'
DISCOVERY_PLATFORMS = {
discovery.SERVICE_CAST: 'cast',
discovery.SERVICE_SONOS: 'sonos',
discovery.SERVICE_PLEX: 'plex',
discovery.SERVICE_SQUEEZEBOX: 'squeezebox',
discovery.SERVICE_PANASONIC_VIERA: 'panasonic_viera',
discovery.SERVICE_ROKU: 'roku',
}
SERVICE_PLAY_MEDIA = 'play_media'
SERVICE_SELECT_SOURCE = 'select_source'
@@ -285,8 +275,7 @@ def select_source(hass, source, entity_id=None):
def setup(hass, config):
"""Track states and offer events for media_players."""
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS)
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
component.setup(config)
@@ -11,7 +11,7 @@ from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
MediaPlayerDevice)
SUPPORT_STOP, MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
STATE_UNKNOWN)
@@ -21,7 +21,7 @@ CONF_IGNORE_CEC = 'ignore_cec'
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA
SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA | SUPPORT_STOP
KNOWN_HOSTS = []
DEFAULT_PORT = 8009
@@ -241,6 +241,10 @@ class CastDevice(MediaPlayerDevice):
"""Send pause command."""
self.cast.media_controller.pause()
def media_stop(self):
"""Send stop command."""
self.cast.media_controller.stop()
def media_previous_track(self):
"""Send previous track command."""
self.cast.media_controller.rewind()

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