Compare commits

..

267 Commits
0.44 ... 0.46

Author SHA1 Message Date
Paulus Schoutsen
e9f273e7e0 Merge pull request #7866 from home-assistant/release-0-46
0.46
2017-06-03 19:16:35 -07:00
Paulus Schoutsen
7ebf36bb70 Fix MQTT camera test (#7878) 2017-06-03 18:57:05 -07:00
Anders Melchiorsen
4dc4a98caa [light.lifx] Update aiolifx (#7882)
This makes LIFX Gen3 lights work with the current firmware.
2017-06-03 13:21:31 +01:00
Paulus Schoutsen
a79f1d4d40 Fix telegram_bot (#7877) 2017-06-03 10:52:00 +01:00
Paulus Schoutsen
a33bcdf270 Version bump to 0.46 2017-06-02 00:24:19 -07:00
Paulus Schoutsen
f056cbc641 Update frontend 2017-06-02 00:20:53 -07:00
Paulus Schoutsen
4163bcebbc Update netdisco (#7865) 2017-06-02 00:13:17 -07:00
Johan Bloemberg
d472d81538 Align switch group handling with light. (#7577) 2017-06-02 00:05:34 -07:00
David-Leon Pohl
2b70b1881a Quickfix Bug #7384 (#7582)
* Quickfix Bug #7384

* Fix devices not available runtime bug
2017-06-02 00:05:07 -07:00
Juggels
12607aeaea Check if media commands are actually applicable (#7595)
* Check if media commands are actually applicable

- Explicitly allow ‘stop’ and ‘play’ on radio streams
- Disallow media commands when the playlist is empty
- Check if command is supported when calling `turn_on` and `turn_off`

* Suppress UPnP error 701 on media commands

* Clean up soco_filter_upnperror

Clean up soco_filter_upnperror and fix small bug in support_previous_track determination
2017-06-02 00:03:10 -07:00
Erik Eriksson
1855f1ae85 fix for https://github.com/home-assistant/home-assistant/issues/7019 (#7618) 2017-06-02 00:02:26 -07:00
Thibault Cohen
613da308f2 Query in InfluxDB sensor is now templatable (#7634) 2017-06-02 00:01:14 -07:00
Teagan Glenn
cefacf9ce4 Spotify aliases (#7702)
* Alias support for spotify devices

* Fix log

* Formatting/Fixes

* Remove default arg

* Add default keyword

* None check
2017-06-01 23:53:23 -07:00
Alex Harvey
78887c5d5c Start of migration framework, to allow moving of files in the config … (#7740)
* Start of migration framework, to allow moving of files in the config directory to be hidden, ios.conf used as the first one to undergo this change.

* Update const.py

* Update test_config.py

* improvement to syntax
2017-06-01 23:50:04 -07:00
Craig J. Ward
3a92bd78ea fix permissions issue for Insteon Local #6558 (#7860)
* fix unlinked commit

* Update insteon_local.py
2017-06-01 23:36:47 -07:00
Paulus Schoutsen
d0021a6171 Make monkey patch work in Python 3.6 (#7848)
* Make monkey patch work in Python 3.6

* Update dockerfiles back to 3.6

* Lint

* Do not set env variable for dockerfile

* Lint
2017-06-01 23:23:39 -07:00
Anders Melchiorsen
e2cfdbff06 Disallow ambiguous color descriptors in the light.turn_on schema (#7765)
* Disallow ambiguous color descriptors in the light.turn_on schema

* Update tests
2017-06-01 23:05:05 -07:00
abmantis
9480f41210 dont use default for switch name, so that the object id is used (#7845) 2017-06-01 22:58:57 -07:00
Boris K
1b5f6aa1b9 Optimize history_stats efficiency and database usage (#7858) 2017-06-01 22:52:55 -07:00
Eugenio Panadero
2065426b16 log time delay of domain setup in info level (#7808)
* log time delay of domain setup in info level

 * when setup problems appear, it's difficult to debug which are the components that took a lot to set up. This minimal change goes further than the 'slow setup warning' and measures the setup time interval for each domain.

* use timer as in helpers/entity
2017-06-01 22:44:44 -07:00
Adam Mills
beb8c05d91 Use expected behvaior for above/below (#7857) 2017-06-01 22:43:24 -07:00
Adam Mills
cf42303afb Rename time trigger 'after' to 'at' (#7846) 2017-06-01 22:40:27 -07:00
Daniel Perna
4bcbeef480 Bumped pyhomematic version (#7861) 2017-06-01 22:33:53 -07:00
Adam Mills
e0712ba329 Expose the node name on the zwave node entity (#7787) 2017-06-01 22:33:16 -07:00
Fabian Affolter
66d6f5174d Allow 'base_url' (fixes #7784) (#7796) 2017-05-31 09:08:53 -07:00
Marcelo Moreira de Mello
9762e1613d Introduced support to Netgear Arlo Cameras (#7826)
*  Introduced support to Netgear Arlo Cameras

* Using async_setup_platform() and applied other changes

* Removed unecessary variables

* Using asyncio for sensor/arlo

* Update arlo.py

* Removed entity_namespace
2017-05-31 09:25:25 +02:00
Phil Hawthorne
bb92ef5497 Downgrade Docker to Python 3.5 to solve Segmentation Faults (#7799)
Downgrades the Dockerfiles used by Home Assistant to Python 3.5, after
Python 3.6 base image was causing segmentation faults.

See home-assistant/home-assistant#7752
2017-05-30 23:56:20 -07:00
Paulus Schoutsen
9f5bfe28d1 Add initial benchmark framework (#7827)
* Add initial benchmark framework

* Use timer from timeit
2017-05-30 21:34:40 -07:00
Marcelo Moreira de Mello
8ee32a8fbd Added persistent error message if cover.myq fails to load (#7700)
* Show persistent error if cover.myq fails

* Fixed typo on getLogger()

* Added ValueError on except condition

* Make pylint happy

* Removed DEFAULT_ENTITY_NAMESPACE since it is not being used
2017-05-30 23:17:32 +02:00
Fabian Affolter
052cd3fc53 Upgrade PyMVGLive to 1.1.4 (#7832) 2017-05-30 18:26:26 +02:00
Fabian Affolter
0ccaf97924 Update docstrings and log messages (#7709) 2017-05-30 11:52:26 +02:00
happyleavesaoc
96b20b3a97 update snapcast media player (#7079)
* update snapcast

* fix docstrings

* bump dep version

* address snapcast review comments

* add snapcast group volume support

* fix snapcast requirements

* update snapcast client entity id

* snapshot/restore functions

* refactor snapshot/restore services

* clean up

* update snapcast req

* bump version

* fix async updates
2017-05-30 11:34:39 +02:00
Daniel Høyer Iversen
91806bfa2a Flux led fix (#7829)
* Update flux_led.py

* style fix
2017-05-30 10:46:18 +02:00
Fabian Affolter
1c4e097bed Upgrade pysnmp to 4.3.7 (#7828) 2017-05-30 09:08:57 +02:00
Pascal Vizeli
2df6aabbf3 Cleanup telegram / Add url to webhook (#7824)
* Cleanup telegram / Add url to webhook

* fix lint

* Fix lint
2017-05-30 06:55:06 +02:00
John Mihalic
81b2111751 Bump aiohttp to 2.1.0 (#7825) 2017-05-30 06:54:16 +02:00
Eugenio Panadero
f7e0d13fe6 Telegram send image: fix mimetype detection (#7802)
* Add `name` var to BytesIO content to get recognized

Sometimes the python-telegram-bot doesn't recognize the mimetype of the file and looks after a name variable to deduce it. Fixes #7413

* bytesio stream recycle less explicit
2017-05-29 22:59:44 +02:00
Johan Bloemberg
5e5c0daa87 Allow configuring DSMR5 protocol. (#7535)
* Allow configuring DSMR5 protocol.

* Give good example.

* Using dev branch until released upstream.

* Update to dsmr_parser supporting v5 arguments.
2017-05-29 16:19:50 +02:00
Fabian Affolter
a7277db4d7 Upgrade mypy to 0.511 (#7809)
Add an optional extended description…
2017-05-29 15:39:24 +02:00
Fabian Affolter
ba44b7edb3 Upgrade sqlalchemy to 1.1.10 (#7807) 2017-05-29 15:38:56 +02:00
Lev Aronsky
8fcc750998 Added handling of an AssertionError from pxssh failed login (#7750)
* Added handling of an AssertionError from pxssh failed login

* Destory and re-create pxssh instance, to fix behavior upon router restart.
2017-05-29 11:22:20 +02:00
Teagan Glenn
eff619a58f Rest notify data (#7757)
* Rest notify data

* Cleanup

* Fix spaces
2017-05-29 11:20:23 +02:00
Andy Castille
fc1bb58247 Rachio (Sprinklers) (#7600)
* Rachio platform started

* Rachio tests

* detect bad api token

* Documentation, Code cleanup

* Docstrings end with a period, log uses %

* Fix arguments, default run time is now 10 minutes

* Fix typo, remove todo (GH issue exists)

* Revert polymer submodule commit

* Use a RachioPy version with SSL cert validation

* Update requirements
2017-05-29 11:15:27 +02:00
Fabian Affolter
c12b8f763c Upgrade pysnmp to 4.3.6 (#7806) 2017-05-29 10:28:31 +02:00
Dan Cinnamon
ef51d8518a Bump pyenvisalink to version 2.1 (#7803) 2017-05-29 10:27:36 +02:00
Fabian Affolter
8b7894fb86 Upgrade slacker to 0.9.50 (#7797) 2017-05-29 10:26:56 +02:00
Fabian Affolter
010f098df3 Upgrade Sphinx to 1.6.2 (#7805) 2017-05-29 10:26:33 +02:00
Oliver
1f3bb51821 Add Marantz SSDP discovery / Detect error string in AppCommand.xml body (#7779) 2017-05-29 10:26:10 +02:00
CTLS
10367eb250 Fix home/stay in concord232 (#7789) 2017-05-28 12:06:18 +02:00
sander76
7fb5488058 Powerview to async (#7682)
* first commit

* first commit

* first commit

* first commit

* changing requirements

* updated requirements_all.txt

* various changes as suggested in the comments.

* using global values for dict keys.
2017-05-26 22:19:19 +02:00
Paulus Schoutsen
e68bd0457c Fix more deprecation warnings (#7778)
* Remove setting up an hbmqtt broker

* Don't pass loop to web.Application in tests

* Use .query instead of deprecated .GET for aiohttp requests

* Fix closing file resource

* Do not use asyncio mark

* Notify.html5 - PyJWT: Use options to disable verify

* Yamaha: Test was still using deprecated ip

* Remove pytest-asyncio
2017-05-26 13:12:17 -07:00
Eugenio Panadero
910020bc5f Fix Telegram Bot send file to multiple targets, snapshots of HA cameras, variable templating, digest auth (#7771)
* fix double template rendering when messages come from notify.telegram

* fix 'chat' information not present in callback queries

* better inline keyboards with yaml

To make a row of InlineKeyboardButtons you pass:
- a list of tuples like: `[(text_b1, data_callback_b1), (text_b2, data_callback_b2), ...]
- a string like: `/cmd1, /cmd2, /cmd3`
- or a string like: `text_b1:/cmd1, text_b2:/cmd2`

Example:
```yaml
data:
   message: 'TV is off'
   disable_notification: true
   inline_keyboard:
     - TV ON:/service_call switch.turn_on switch.tv, Other:/othercmd
     - /help, /init
```

* fix send file to multiple targets

* fix message templating, multiple file targets, HA cameras

- Allow templating for caption, url, file, longitude and latitude fields
- Fix send a file to multiple targets
- Load data with some retrying for HA cameras, which return 500 one or two times sometimes (generic cams, always!).
- Doc in services for new inline keyboards yaml syntax: `Text button:/command`

* HttpDigest authentication as proposed in #7396

* review changes

- Don't use `file` as variable name.
- For loop
- Simplify filter allowed `chat_id`s.

* Don't use `file` as variable name!

* make params outside the while loop

* fix chat_id validation when editing sent messages
2017-05-26 21:05:12 +02:00
Paulus Schoutsen
f43db3c615 Replace executor with async_add_job (#7658)
* Remove executor

* Lint

* Lint

* Fix tests
2017-05-26 08:28:07 -07:00
Adam Mills
9e9705d6b2 Support for GE Zwave fan controller (#7767)
* Support for GE Zwave fan controller

* Tests for zwave fan

* Add additional fan workarounds
2017-05-25 22:55:00 -07:00
Paulus Schoutsen
6899c7b6f7 assertEquals is deprecated (#7777) 2017-05-25 22:21:22 -07:00
Paulus Schoutsen
d0c9d6b69a Remove usage of event_loop fixture (#7776) 2017-05-25 21:40:36 -07:00
Paulus Schoutsen
81aaeaaf11 Get rid of mock http component app (#7775)
* Remove mock_http_component from config tests

* Remove mock_http_component_app from emulated hue test
2017-05-25 21:13:53 -07:00
Adam Mills
65c3201fa6 Rename of the zwave hass.data constants (#7768)
* Rename of the zwave hass.data constants

* Remove zwave since it is already implied
2017-05-25 21:11:02 -07:00
Anton Sarukhanov
3a843e1817 Add icons to device tracker. (#7759) 2017-05-24 19:12:26 -07:00
Paulus Schoutsen
0c7f8e910e Merge branch 'master' into dev 2017-05-24 19:05:01 -07:00
Hugo Herter
0abde3aa57 Change setup script to use pip install instead of setup.py develop (#7756)
Using `python setup.py develop` did not manage to install the required dependencies.
This updates `script/setup` to use `pip install -e .` instead in order to resolve the required dependencies.
2017-05-24 15:31:51 -07:00
amigian74
775d45ae5a Exclude filter for event types (#7627)
* add exclude filter for event types to recorder component

* corrected long line (279)

* change source code structure
add test for exclude event types

* code cleanup

* change source code structure

* Update __init__.py

* Update test_init.py
2017-05-24 15:23:52 -07:00
Paulus Schoutsen
e7d783ca2a Update links.html 2017-05-24 14:47:22 -07:00
cribbstechnologies
ef4ef2d383 Template light (#7657)
* starting light template component

* linting/flaking

* starting unit tests from copypasta

* working on unit testing

* forgot to commit the test

* wrapped up unit testing

* adding remote back

* updates post running tox

* Revert "adding remote back"

This reverts commit 852c87ff96.

* adding submodule back from origin

* updating submodule

* removing a line to commit

* re-adding line

* trying to update line endings

* trying to fix line endings

* trying a different approach

* making requested changes, need to fix tests

* flaking

* union rather than intersect; makes a big difference

* more tests passing, not sure why this one's failing

* got it working

* most of the requested changes

* hopefully done now

* sets; the more you know
2017-05-24 14:32:22 -04:00
everix1992
3638b21bcb Added new commands and functionality to the harmony remote component. (#7113)
* Added new commands and functionality to the harmony remote component.

-This includes the ability to optionally specify a number of times to repeat a specific command, such as pressing the volume button multiple times.
-Also added a new command that allows you to send multiple commands to the harmony at once, such as sending a set of channel numbers.
-Updated the unit tests for these changes.

* Fix flake8 coding violations

* Remove send_commands command and make send_command handle a single or list of commands

* Remove send_commands tests

* Update itach and kira remotes for new send_command structure. Fix pyharmony version in requirements_all.txt

* Fix incorrect variable name

* Fix a couple minor issues with remote tests
2017-05-23 17:00:52 -07:00
Stu Gott
54c45f80c1 Fix time_date sensor to update at predictable intervals (#7644)
* Fix time_date sensor to update at predictable intervals

* Delete automations.yaml
2017-05-23 16:00:26 -07:00
Juggels
e3307fb1c2 Redesign monitored variables for hp_ilo sensor (#7534)
* Redesign monitored variables

Allow generating specific sensors without the need for template sensors

* Import 3rd party library inside update method

* Remove jsonpath_rw dependency

* Do not interfere with value_template or ilo_data output

Do not interfere with value_template or ilo_data output, this is now the responsibility of the user and should be handled in `configuration.yaml`

Fix UnusedImportStatement

Fix newline after function docstring

* Always output results to state
2017-05-23 14:56:00 -07:00
William Scanlon
b5f20c9b64 Always return rgb color of bulbs (#7743) 2017-05-23 14:49:20 -07:00
Anton Sarukhanov
7055fddfb4 Don't block startup more than 60 seconds while waiting for components. (#7739) 2017-05-23 14:29:27 -07:00
Anders Melchiorsen
fce09f624b LIFX: disable color features for white-only bulbs (#7742)
The product type is already established in order to decide the Kelvin range
so just reuse that information to disable color features for white-only lights.

Also change the breathe/pulse effects to be more useful for white-only
bulbs. For consistency, color bulbs set to a desaturated (i.e. white-ish)
color get the same default treatment as white-only bulbs.
2017-05-23 22:35:19 +02:00
nordeep
be53cc7068 Don't initialize mqtt components which have already been discovered (#7625)
* Don't initialize mqtt components which have already been discovered

* Fix string length

* Fix blank lines, fix constant name

* Remove globals. Remove JSON dump

* Add tests. Update grammar

* PEP8 style issue

* Add hyphen to object_id regex

* PEP8 style fix
2017-05-23 11:08:12 -07:00
Anton Sarukhanov
f3dabe21ab Prevent the random template filter from caching its output. Fixes #5678 (#7716) 2017-05-23 10:32:06 -07:00
Brenton Zillins
228fb8c072 Ensure https base_url in telegram bot (#7726) 2017-05-23 10:16:54 -07:00
Lev Aronsky
c556b619b7 Asuswrt continuous ssh (#7728)
* Make ssh and telnet connections continuous in asuswrt

* Refactored SSH and Telnet connections into respective classes.

* Fixed several copy-paste typos and errors.

* More typos fixed.

* Small changes to arguments, to pass automated tests.

* Removed unsupported named arguments.

* Fixed a couple of mistakes in Telnet, and other lint errors.

* Added Telnet tests, and added lint exceptions.

* Removed comments from tests, as they irritated the hound.
2017-05-23 09:55:01 -07:00
Paulus Schoutsen
2682996939 Constrain requests to a version (#7725)
Add an optional extended description…
2017-05-23 15:45:22 +02:00
Alex Harvey
6872daab89 update apcacccess used in apcupsd to 0.0.10, which fixes random file drop from apcaccess (#7722) 2017-05-22 17:00:41 -07:00
Paulus Schoutsen
6d183e8bb3 Merge pull request #7686 from home-assistant/release-0-45-1
0.45.1
2017-05-22 11:36:21 -07:00
Paulus Schoutsen
cdc8628e5a Allow fetching hass.io panel without auth (#7714) 2017-05-22 11:06:04 -07:00
tobygray
dc4b0695b5 device_tracker.ubus: Handle empty results (#7673)
If OpenWRT isn't running the DHCP server then some OpenWRT hardware,
such as TP-Link TL-WDR3600 v1, can't determine the host
corresponding to an associated wifi client. This change handles that
by returning None when the request has no data in the result.
2017-05-22 11:06:04 -07:00
cgtobi
3fb691ead6 Fix playback control of web streams (#7683)
Web streams can't be paused and resumed later. That's why volumio stops them instead of pausing them.
2017-05-22 11:06:04 -07:00
Eugenio Panadero
a9926e355f Fix telegram chats (#7689)
* bugfix for Telegram chat_ids

- Negative `chat_id`s for groups.
- Include `chat_id` in event data.
- Handle KeyError when receiving other types of messages, as `new_chat_member` ones, and send them as text.

* unused import

* fix double quote style, fix boolean expr, change warning msg

* mistake

* some more fixes

- fix if condition for msg bad fields.
- return True for a correct but not allowed or not recognized message: if not, the message arrives continuously.
- Allow to receive messages from unauthorized users if they come from authorized groups.

* support for `edited_message`s

- They come as normal messages, except for the 'edited_message' field instead of 'message'.
2017-05-22 11:06:04 -07:00
Paulus Schoutsen
17cbe0c6ce Allow fetching hass.io panel without auth (#7714) 2017-05-22 11:00:02 -07:00
Fabian Affolter
783abc7996 Make 'sender' as requirement for the config (fixes #7698) (#7706) 2017-05-22 15:17:15 +02:00
Fabian Affolter
47355eed41 Upgrade python-telegram-bot to 6.0.1 (#7704) 2017-05-22 13:56:36 +02:00
John Mihalic
d5642a5faf Bump pyEight version (#7701) 2017-05-22 07:54:01 +02:00
tobygray
ca3f07cdef device_tracker.ubus: Handle empty results (#7673)
If OpenWRT isn't running the DHCP server then some OpenWRT hardware,
such as TP-Link TL-WDR3600 v1, can't determine the host
corresponding to an associated wifi client. This change handles that
by returning None when the request has no data in the result.
2017-05-21 17:26:05 -07:00
LvivEchoes
99ea1e3f4f Continue tracking device over dhcp lease table if wireless adapter not installed (#7690) 2017-05-21 17:18:55 -07:00
Anders Melchiorsen
bb8de5845a Sort entities in default groups by name (#7681)
* Sort entities in default groups by name

* Cleanups from review
2017-05-21 17:05:48 -07:00
cgtobi
b3cb057aac Fix playback control of web streams (#7683)
Web streams can't be paused and resumed later. That's why volumio stops them instead of pausing them.
2017-05-21 17:05:04 -07:00
Eugenio Panadero
922303fd4b Fix telegram chats (#7689)
* bugfix for Telegram chat_ids

- Negative `chat_id`s for groups.
- Include `chat_id` in event data.
- Handle KeyError when receiving other types of messages, as `new_chat_member` ones, and send them as text.

* unused import

* fix double quote style, fix boolean expr, change warning msg

* mistake

* some more fixes

- fix if condition for msg bad fields.
- return True for a correct but not allowed or not recognized message: if not, the message arrives continuously.
- Allow to receive messages from unauthorized users if they come from authorized groups.

* support for `edited_message`s

- They come as normal messages, except for the 'edited_message' field instead of 'message'.
2017-05-21 17:02:22 -07:00
Adam Mills
8c1181f8e3 Remove defunct INSTALL_OPENZWAVE from Dockerfile (#7697) 2017-05-21 17:01:42 -07:00
John Arild Berentsen
4a0d6e73f4 ZWave: Add reset service to meters (#7676)
* Add reset service for command_class meters.

* Add reset service for command_class meters.

* cast index to const.py
2017-05-21 20:15:24 +02:00
Paulus Schoutsen
171086229a Guard against new and removed state change events (#7687) 2017-05-21 07:41:33 -07:00
Andrey
927024714b Zwave: Apply refresh_node workaround on 1st instance only (#7579)
* Apply refresh_node workaround on 1st instance only

* Add another test
2017-05-21 17:33:42 +03:00
tobygray
24b7fd3694 zoneminder: fix incorrect use of logging.exception. (#7675)
Prior to this change the zoneminder component was attempting to
use logging.exception outside of exception handling code. This
would lead to the traceback module throwing an exception when
trying to work out the traceback for the exception.

This fixes the issue by changing the exception call into a
plain error logging call.
2017-05-21 11:11:33 +02:00
Paulus Schoutsen
d6f43ba839 Version bump to 0.45.1 2017-05-20 22:34:59 -07:00
Paulus Schoutsen
3492545ec1 Update state automation to work with new and deleted state changes 2017-05-20 22:34:53 -07:00
Fabian Affolter
ceff9981be Merge branch 'master' into dev 2017-05-21 00:47:42 +02:00
Fabian Affolter
70ea16bdc0 Merge pull request #7648 from home-assistant/release-0-45
0.45
2017-05-21 00:34:44 +02:00
Marcelo Moreira de Mello
943958b140 Added support to Amcrest camera to feed using RTSP via ffmpeg (#7646)
* Implemented ffmpeg option on Amcrest camera and upgraded to version 1.2.0

* Added ffmpeg arguments and binary options to Amcrest camera

* Added ffmpeg as dependencies

* Makes lint happy and fixed requirements_all.txt

* Inherent the ffmpeg.binary configuration from ffmpeg component

* Update amcrest.py
2017-05-20 23:55:15 +02:00
John Arild Berentsen
23c5fc0aad Bugfix #7586 (#7661) 2017-05-20 23:53:48 +02:00
Fabian Affolter
45b4ef46cc Align with OpenALPR platform for naming conf variables (#7650) 2017-05-20 23:51:16 +02:00
Andrey
44edf3e105 Switch pymodbus to pypi (#7677) 2017-05-20 21:19:22 +02:00
Anders Melchiorsen
81f0826550 Ignore attribute changes in automation trigger from/to (#7651)
* Ignore attribute changes in automation trigger from/to

* Quote names in deprecation warnings

This makes it somewhat easier to read if the suggestion happens to be
named "to".

* Add test with same state, new attribute value
2017-05-20 15:18:59 -04:00
Barry Williams
adde9e6231 Upgrade Openhome library (#7671)
* Added support for openhome devices using transport service

* Style cleanup
2017-05-20 17:43:35 +02:00
Paulus Schoutsen
de85d38aa5 Update frontend 2017-05-20 08:08:06 -07:00
Paulus Schoutsen
f637a07016 Update frontend 2017-05-20 08:07:32 -07:00
thecynic
9e153119ef Point pylutron to pypi (#7664) 2017-05-20 14:27:35 +03:00
Fabian Affolter
b5c54864ac Change line endings to LN (#7660) 2017-05-19 07:39:13 -07:00
Paulus Schoutsen
d369d70ca5 Fix tests (#7659)
* Remove global hass

* Http.auth test no longer spin up server

* Remove server usage from http.ban test

* Remove setupModule from test device_sun_light_trigger

* Update common.py
2017-05-19 07:37:39 -07:00
John Arild Berentsen
5aa72562a7 Bugfix #7586 (#7661) 2017-05-19 13:40:26 +02:00
Robbie Trencheny
c4da921cb5 Add network_key as a config option (#7637)
* Add network_key as a config option

* Update __init__.py
2017-05-18 23:49:37 -07:00
Robbie Trencheny
7daa92249a Add network_key as a config option (#7637)
* Add network_key as a config option

* Update __init__.py
2017-05-18 23:49:15 -07:00
John Arild Berentsen
4a3d9a956d Final tweaks for Zwave panel (#7652)
* # This is a combination of 3 commits.
# The first commit's message is:
Add seperate zwave panel

# The 2nd commit message will be skipped:

#	unused import

# The 3rd commit message will be skipped:

#	Use get for config

* Add seperate zwave panel

* Modify set_config_parameter to accept setting string values

* descriptions

* Tweaks

* Tweaks

* Tweaks

* Tweaks

* lint

* Fallback if no config parameteres are available

* Update services.yaml

* review changes
2017-05-18 17:41:31 -07:00
Paulus Schoutsen
6662b7f52d Update frontend 2017-05-18 17:41:26 -07:00
Paulus Schoutsen
e91fe94585 Update frontend 2017-05-18 17:41:03 -07:00
John Arild Berentsen
88ffe39945 Final tweaks for Zwave panel (#7652)
* # This is a combination of 3 commits.
# The first commit's message is:
Add seperate zwave panel

# The 2nd commit message will be skipped:

#	unused import

# The 3rd commit message will be skipped:

#	Use get for config

* Add seperate zwave panel

* Modify set_config_parameter to accept setting string values

* descriptions

* Tweaks

* Tweaks

* Tweaks

* Tweaks

* lint

* Fallback if no config parameteres are available

* Update services.yaml

* review changes
2017-05-18 17:39:31 -07:00
happyleavesaoc
e479324db9 update usps (#7655)
* update usps

* fix doc
2017-05-18 17:30:43 -07:00
happyleavesaoc
f65cc68705 bump ups version (#7654) 2017-05-18 23:38:50 +02:00
happyleavesaoc
238921b681 bump fedex version (#7653) 2017-05-18 23:37:39 +02:00
Marcelo Moreira de Mello
0fd415d7fb Added support to Amcrest camera to feed using RTSP via ffmpeg (#7646)
* Implemented ffmpeg option on Amcrest camera and upgraded to version 1.2.0

* Added ffmpeg arguments and binary options to Amcrest camera

* Added ffmpeg as dependencies

* Makes lint happy and fixed requirements_all.txt

* Inherent the ffmpeg.binary configuration from ffmpeg component

* Update amcrest.py
2017-05-18 10:06:24 +02:00
Fabian Affolter
0eb6540fe7 Align with OpenALPR platform for naming conf variables (#7650) 2017-05-18 09:57:38 +02:00
Paulus Schoutsen
fc0c8540d3 Version bump to 0.46.0.dev0 2017-05-17 23:03:47 -07:00
Paulus Schoutsen
eb473600f6 Version bump to 0.45 2017-05-17 23:03:27 -07:00
Paulus Schoutsen
de999d8439 Merge remote-tracking branch 'origin/master' into dev 2017-05-17 23:03:00 -07:00
Paulus Schoutsen
6d97546f40 Update frontend 2017-05-17 22:29:46 -07:00
Paulus Schoutsen
e773133bcf Fix automation failing to setup if no automations specified (#7647) 2017-05-17 21:57:50 -07:00
Anders Melchiorsen
3d4b2436db Coerce color_temp to int even when passed in as kelvin (#7640) 2017-05-17 19:20:59 -07:00
Paulus Schoutsen
a068efcd47 Abort tests when instances leaked (#7623) 2017-05-18 00:19:40 +02:00
Fabian Affolter
f86edd4f24 Seven segments OCR image processing (#7632)
* Add initial seven segments OCR image processing

* Fix typo
2017-05-18 00:07:02 +02:00
Daniel Perna
a24aebd5ae Updated dependency (#7638) 2017-05-17 23:57:34 +02:00
Riccardo Canta
f3b9e1e988 Osram lightify Removed wrong assignment (#7615)
self._brightness is assigned with the returned value of the
set_luminance() function, which is always equal to None.
2017-05-17 23:26:58 +02:00
corneyl
76b747edd6 Updated limitlessled requirement to v1.0.8 (#7629) 2017-05-17 09:05:34 -07:00
Eugenio Panadero
f7d25396a4 Kodi specific service to call Kodi API methods (#7603)
* Kodi specific services to call Kodi API methods

 - new service: `kodi_execute_addon` to run a Kodi Addon with optional parameters. Results of the Kodi API call, if any, are redirected in a Home Assistant event: `kodi_execute_addon_result`.
 - new service: `kodi_run_method` to run a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call are redirected in a Home Assistant event: `kodi_run_method_result`.
 - Add descriptions in services.yaml.
 - Add `timeout` parameter to yaml config (needed to make slow queries to the JSONRPC API, default timeout is set to 5s).
 - Trigger events with the results of the Kodi API calls, with:
 ```
 event_data = {
   'result': api_call_results,
   'result_ok': boolean,
   'input': api_call_parameters,
   'entity_id': 'media_player.kodi'}
```

* no need to clean OrderedDicts; no need for the `kodi_execute_addon` service

* no need for the `kodi_execute_addon` service

* unused import

* naming changes
2017-05-17 08:42:47 -04:00
Fabian Affolter
0e9728d94a Update docstrings (#7630) 2017-05-17 10:10:35 +02:00
Fabian Affolter
3b69de8a1a Upgrade Sphinx to 1.6.1 (#7624) 2017-05-17 09:14:28 +02:00
Fabian Affolter
f0b2a6d0e6 Update docstrings and comments (#7626) 2017-05-17 09:14:11 +02:00
Conrad Juhl Andersen
d2ed3a131f Add support for disabling tradfri groups (#7593)
* Add support for disabling tradfri groups

* Fixed styleguide problems

* Fix style problems

* Use default for groups when setting up in UI
2017-05-16 23:26:57 -07:00
Anders Melchiorsen
ed5f94fd8a Add kelvin/brightness_pct alternatives to light.turn_on (#7596)
* Refactor color profiles to a class

* Refactor into preprocess_turn_on_alternatives

* LIFX: use light.preprocess_turn_on_alternatives

This avoids the color_name duplication and gains support for profile.

* Add kelvin parameter to light.turn_on

* Add brightness_pct parameter to light.turn_on

* LIFX: accept brightness_pct in effects

* Add test of kelvin/brightness_pct conversions
2017-05-16 23:00:46 -07:00
Greg Dowling
9dcc0b5ef5 Bump pyvera - fixes issue with % in brightness levels. (#7622) 2017-05-16 20:01:29 -07:00
Fabian Affolter
1fafa34eb1 Fix typo and update style to match the other platforms (#7621) 2017-05-16 21:57:00 +02:00
Ole-Kenneth
71ed17b836 Add Content-type: image/jpeg for camera proxy (#7581)
* Add Content-type: image/jpeg for camera proxy

* Set content_type in constructur
2017-05-16 09:11:44 -07:00
Philipp Schmitt
a7f933966b Update Docker base image to python 3.6 (#7613) 2017-05-16 09:09:21 -07:00
Paulus Schoutsen
641ba014f2 Update frontend 2017-05-15 23:17:33 -07:00
Paulus Schoutsen
d5ca6a5aed Force automation ids to always be a string (#7612) 2017-05-15 23:15:06 -07:00
Marc Egli
d6081f3dc5 Make miflora monitored_conditions parameter optional (#7598)
* Make miflora monitored_conditions parameter optional.

* Use default keyword instead.

* Use list instead of tuple

* Simplify even more
2017-05-15 23:13:41 -07:00
Fabian Affolter
f25347d98d File sensor (#7569)
* Add File sensor

* Use None and return

* Remove I/O

* Use less memory

* No traceback if file is empty
2017-05-15 14:25:46 +02:00
John Mihalic
a1dc35fc75 Fix handling of single user (#7587) 2017-05-15 00:46:43 -07:00
Marc Egli
4da91d6a8b Add sonos alarm clock update service (#7521)
* Add sonos alarm clock update service

* Add tests and break lines

* Fix style errors

* Make test work with python<3.6

* Fix last two pylint errors

* fix new line to long errors
2017-05-15 00:42:45 -07:00
jhemzal
5c4a21efac Add posibility to specify snmp protocol version (#7564)
* snmp sensor protocol version configuration option

* fixed lint findings.
2017-05-15 00:34:51 -07:00
Adam Mills
e2e58e6acc Automation State Change For timer attribute fix (#7584) 2017-05-15 00:34:30 -07:00
Eugenio Panadero
d0304198de SMTP notify enhancements: full HTML emails and custom product_name in email headers (#7533)
* SMTP notify enhancements: HTML emails and customization options

- Send full HTML emails, with or without inline attached images.
- Custom `timeout`.
- Custom `product_name` identifier for the `FROM` and `X-MAILER` email headers.
- New HTML email test

* `sender_name` instead of product_name

 - Change `sender_name` instead of `product_name`.
 - No changes in `X-Mailer` header.
 - `From` header as always unless you define the new `sender_name` parameter.
2017-05-15 00:23:57 -07:00
Paulus Schoutsen
36d7fe72eb Fix websocket api reaching queue (#7590)
* Fix websocket api reaching queue

* Fix outside task message sending

* Fix Py34 tests
2017-05-15 00:10:06 -07:00
Marc Egli
6d245c43fc Pass additional arguments to tox in test_docker (#7591) 2017-05-14 23:21:39 -07:00
Matt N
e1a4d51fa2 camera.zoneminder: Handle old versions of zoneminder (#7589) 2017-05-13 23:09:44 -07:00
Paulus Schoutsen
352cca1037 Remove more test requirements (#7574)
* No longer require pyunify during tests

* No longer require cast during tests

* No longer required dependency for tests

* No longer require pymochad for tests

* Astral is a core dependency

* Avoid having to install datadog dependency during tests

* CMUS test doesn't test anything

* Frontier Silicon doesn't test anything

* No longer require mutagen

* Update requirements_test_all.txt

* Remove stale comment
2017-05-13 21:25:54 -07:00
Paulus Schoutsen
206d02d531 Websocket_api: avoid parallel drain (#7576)
* Websocket_api: avoid parallel drain

* Remove send_message method
2017-05-13 16:34:45 -07:00
William Scanlon
cfbbade6d1 Additional Wink lock features (#7445)
* Additional Wink lock features
2017-05-13 14:09:00 -04:00
Adam Mills
cfea4b17e3 Add tests for zwave network events (#7573) 2017-05-12 23:06:32 -07:00
Stu Gott
9c4bc2a47f Add Kira component to sensor and remote platforms (#7479)
* Add Kira component to sensor and remote platforms

* Test cases for Kira component and platforms
2017-05-12 21:12:47 -07:00
Eugenio Panadero
4cdf0b4969 Fix Kodi specific services registry and add descriptions (#7551)
* Fix Kodi specific services, add descriptions, add more handled exceptions

 - Fixes issue #7528
 - Add descriptions for Kodi specific services in services.yaml.
 - Error handling in Kodi API errors.
 - Make compatible the existent specific service `media_player.kodi_set_shuffle` with the general `media_player.shuffle_set` service (both use the same method but with different named parameter, I think the Kodi specific service should be eliminated, since it is not)

* fix line too long

* removed new services (for another PR); removed `kodi_set_shuffle` service

* requested changes

 - Removed `kodi_set_shuffle` service.
 - Optional `media_name` and `artist_name` parameters. `media_name` defaults to 'ALL'.
 - Guard clause to check if the services are already registered.
2017-05-12 20:48:57 -07:00
bestlibre
ad15844cf4 Fix systematic warning in influxdb sensor (#7541) 2017-05-12 20:47:12 -07:00
Kevin Fronczak
25cb7c652b Blink version bump (#7571)
Bumped blink version to support automatic reauthorization when tokens expire. Changed the battery sensor call to a string version so that the battery reports back "Low" or "OK" rather than a cryptic integer
2017-05-12 20:30:07 -07:00
Adam Mills
189023821b Tests for zwave setup features (#7570)
* Tests for zwave setup features

* Add test for frontend panel register
2017-05-12 20:27:44 -07:00
Adam Mills
c118be6639 Tests for zwave discovery logic (#7566)
* Tests for zwave discovery logic

* Simplify patching

* Test ignored node
2017-05-12 20:18:20 -07:00
Mitesh Patel
11a3dc268f Support lutron serena shades (#7565)
* Adds support for the Lutron Caseta Serena shades hardware

* fixes typos
2017-05-12 20:17:11 -07:00
Paulus Schoutsen
f0ce6c8210 Update netdisco (#7563) 2017-05-12 20:14:17 -07:00
Juggels
ed0ec613c3 Comment RasPi specific requirements (#7562) 2017-05-12 20:06:28 -07:00
Johan Bloemberg
4a3048b370 Initialize sun with correct values. (#7559)
* Initialize sun with unknown values.

Initial values should be `unknown` instead of `0`. Otherwise on HA restart the value of `0` is pushed to metrics databases (graphite/influx/recorder).

* Update sun position before emitting initial update

* Simplify based on armills comment.

* Use provided time for calculation.
2017-05-12 16:04:30 -07:00
Per Osbäck
fdb7371256 update pywebpush to 1.0.0 (#7561) 2017-05-12 09:25:34 -07:00
florincosta
a96a98a260 Add raspihats binary sensor (#7508)
* Added raspihats binary_sensor platform

* Updated .coveragerc to ommit raspihats platforms.

* Using vol.Coerce(int) for validation and casting of I2CHat config address
2017-05-12 09:20:48 -07:00
Anders Melchiorsen
1ab7103aea LIFX: add lifx_set_state service call (#7552)
* Move service helpers to LifxManager

* Add lifx_set_color

This is a synonym for light.turn_on except it does not actually turn on the
light unless asked to.

Thus, turn_on can be implemented just by asking.

* Rename set_color to set_state

* Support power=False with lifx_set_state
2017-05-12 09:19:51 -07:00
Kane610
416b8e0efe Axis component (#7381)
* Added Axis hub, binary sensors and camera

* Added Axis logo to static images

* Added Axis logo to configurator
Added Axis mdns discovery

* Fixed flake8 and pylint comments

* Missed a change from list to function call
V5 of axis py

* Added dependencies to requirements_all.txt

* Clean up

* Added files to coveragerc

* Guide lines says to import function when needed, this makes Tox pass

* Removed storing hass in config until at the end where I send it to axisdevice

* Don't call update in the constructor

* Don't keep hass private

* Unnecessary lint ignore, following Baloobs suggestion of using NotImplementedError

* Axis package not in pypi yet

* Do not catch bare excepts. Device schema validations raise vol.Invalid.

* setup_device still adds hass object to the config, so the need to remove it prior to writing config file still remains

* Don't expect axis.conf contains correct values

* Improved configuration validation

* Trigger time better explains functionality than scan interval

* Forgot to remove this earlier

* Guideline says double qoutes for sentences

* Return false from discovery if config file contains bad data

* Keys in AXIS_DEVICES are serialnumber

* Ordered imports in alphabetical order

* Moved requirement to pypi

* Moved update callback that handles trigger time to axis binary sensor

* Renamed configurator instance to request_id since that is what it really is

* Removed unnecessary configurator steps

* Changed link in configurator to platform documentation

* Add not-context-manager (#7523)

* Add not-context-manager

* Add missing comma

* Threadsafe configurator (#7536)

* Make Configurator thread safe, get_instance timing issues breaking configurator working on multiple devices

* No blank lines allowed after function docstring

* Fix comment Tox

* Added Axis hub, binary sensors and camera

* Added Axis logo to static images

* Added Axis logo to configurator
Added Axis mdns discovery

* Fixed flake8 and pylint comments

* Missed a change from list to function call
V5 of axis py

* Added dependencies to requirements_all.txt

* Clean up

* Added files to coveragerc

* Guide lines says to import function when needed, this makes Tox pass

* Removed storing hass in config until at the end where I send it to axisdevice

* Don't call update in the constructor

* Don't keep hass private

* Unnecessary lint ignore, following Baloobs suggestion of using NotImplementedError

* Axis package not in pypi yet

* Do not catch bare excepts. Device schema validations raise vol.Invalid.

* setup_device still adds hass object to the config, so the need to remove it prior to writing config file still remains

* Don't expect axis.conf contains correct values

* Improved configuration validation

* Trigger time better explains functionality than scan interval

* Forgot to remove this earlier

* Guideline says double qoutes for sentences

* Return false from discovery if config file contains bad data

* Keys in AXIS_DEVICES are serialnumber

* Ordered imports in alphabetical order

* Moved requirement to pypi

* Moved update callback that handles trigger time to axis binary sensor

* Renamed configurator instance to request_id since that is what it really is

* Removed unnecessary configurator steps

* Changed link in configurator to platform documentation

* No blank lines allowed after function docstring

* No blank lines allowed after function docstring

* Changed discovery to use axis instead of axis_mdns

* Travis CI requested rerun of script/gen_requirements_all.py
2017-05-12 08:51:54 -07:00
Andrey
5b3ef0f76f Treat swing and fan level as optional in Sensibo Climate. (#7560) 2017-05-12 18:28:58 +03:00
Tsvi Mostovicz
452c3a1b25 Support adding different server locations for Microsoft face component (#7532)
* Support adding different server locations

* Rename variables and move CONF_ const into component as requested in review

* Fix unittests

* Forgot to add tests for microsoft_face_identify
2017-05-12 10:53:25 +02:00
Paulus Schoutsen
8da10f670b Only install tox in dev mode (#7557) 2017-05-12 00:01:06 -07:00
Adam Mills
b805d8a844 Hide proximity updates in logbook (#7549) 2017-05-11 19:37:32 -07:00
Paulus Schoutsen
76675a54f8 Do not install all dependencies in dev mode (#7548)
* ps - do not install all dependencies

* Comment out blinkt because it depends on GPIO

* Add pip upgrade check back

* Disable import error blinkt

* Update comment

* Fix comment
2017-05-11 19:20:23 -07:00
Trevor
0e246059f9 Add SSL support to NZBGet sensor (#7553) 2017-05-11 23:05:06 +02:00
Fabian Affolter
0e41342a40 Upgrade dweepy to 0.3.0 (#7550) 2017-05-11 22:48:03 +02:00
Adam Mills
04f1054d07 Automatic version bump (#7555) 2017-05-11 22:47:47 +02:00
Fabian Affolter
966bda079e Upgrade sendgrid to 4.1.0 (#7538) 2017-05-11 09:06:22 -07:00
jumpkick
ef4587f994 Fix for #7459 (#7544)
* Generate a new updateDate with every call

This should fix #7459
Tests need to be updated in another commit.

* Replace STATIC_TIME with datetime object check

Removing the "DATE" argument from the Alexa component's configuration (because it is now dynamically generated) requires this commit's changes to the test cases to check that the updateDate data is a datetime type rather than a specific hardcoded value ('2016-10-10T19:51:42.0Z').

* Fix brackets
2017-05-11 09:04:17 -07:00
Kane610
2c8f6a0ad0 Threadsafe configurator (#7536)
* Make Configurator thread safe, get_instance timing issues breaking configurator working on multiple devices

* No blank lines allowed after function docstring

* Fix comment Tox
2017-05-11 10:24:36 +03:00
Fabian Affolter
8cdadd2aa0 Add not-context-manager (#7523)
* Add not-context-manager

* Add missing comma
2017-05-11 09:14:52 +02:00
Fabian Affolter
3bdf77ad62 Add myStrom binary sensor (#7530) 2017-05-10 16:58:03 +02:00
Adam Mills
8c90fd19ff Try to request current_location Automatic scope (#7447) 2017-05-10 05:44:52 -07:00
Fabian Affolter
71b4afb780 Update docstrings and log messages (#7526) 2017-05-10 12:06:57 +02:00
corneyl
6e6a000217 Upgrade limitlessled to 1.0.7 (#7525) 2017-05-10 10:45:33 +02:00
Bas Schipper
85e71fc785 Support for the PiFace Digital I/O module (#7494)
* Added rpi_pfio component supporting the PiFace I/O module

* Fixed some code style issues

* Removed global listener

* Update rpi_pfio.py
2017-05-09 22:36:33 -07:00
Fabian Affolter
216199556a Don't interact with hass directly (#7099) 2017-05-09 21:56:17 -07:00
Nuno Sousa
89d950c73a Add password parameter to uvc component (#7499) 2017-05-09 21:54:38 -07:00
Eugenio Panadero
b30c352e37 Telegram Bot enhancements with callback queries and new notification services (#7454)
* telegram_bot and notify.telegram enhancements:
- Receive callback queries and produce `telegram_callback` events.
- Custom reply_markup (keyboard or inline_keyboard) for every type of message (message, photo, location & document).
- `disable_notification`, `disable_web_page_preview`, `reply_to_message_id` and `parse_mode` optional keyword args.
- Line break between title and message fields: `'{}\n{}'.format(title, message)`
- Move Telegram notification services to `telegram_bot` component and forward service calls from the telegram notify service to the telegram component, so now the `notify.telegram` platform depends of `telegram_bot`, and there is no need for `api_key` in the notifier configuration. The notifier calls the new notification services of the bot component:
	- telegram_bot/send_message
	- telegram_bot/send_photo
	- telegram_bot/send_document
	- telegram_bot/send_location
	- telegram_bot/edit_message
	- telegram_bot/edit_caption
	- telegram_bot/edit_replymarkup
	- telegram_bot/answer_callback_query
- Added descriptions of the new notification services with a services.yaml file.
- CONFIG_SCHEMA instead of PLATFORM_SCHEMA for the `telegram_bot` component, so only one platform is allowed.
- Async component setup.

* telegram_bot and notify.telegram enhancements: change in requirements_all.txt.
2017-05-09 21:42:17 -07:00
Gergely Imreh
1312ee0f7d sensor.envirophat: do not set up platform if hardware is not attached (#7438)
* sensor.envirophat: do not set up platform if hardware is not attached

Fixes comment from:
https://github.com/home-assistant/home-assistant/pull/7427#discussion_r114703904

* Fix update logic.
2017-05-09 21:29:38 -07:00
Andrey
f4915ddb0b Switch basicmodem and python-roku to pypi (#7514) 2017-05-09 20:23:19 -07:00
Marc Egli
43296069c3 Update docker dev environment to python3.6 (#7520)
* Update docker dev environment to python3.6

* comment out disable switches again
2017-05-09 20:16:46 -07:00
John Arild Berentsen
1eaec8f406 Zwave panel api (#7456)
* # This is a combination of 3 commits.
# The first commit's message is:
Add seperate zwave panel

# The 2nd commit message will be skipped:

#	unused import

# The 3rd commit message will be skipped:

#	Use get for config

* Add seperate zwave panel

* more info

* Add usercodeview

* Improve api

* Improve api

* Separate api into own file.

* disable missing import

* review changes

* Tests 1

* Verify that we fetch data from groups

* Tests groups

* config 1

* usercode 1

* Api mods

* Tweak API

* docstrings

* 100% api testing
2017-05-09 18:56:41 -07:00
Paulus Schoutsen
5d820ec188 Add support for automation config panel (#7509)
* Add support for automation config

* Build fromtend

* Lint
2017-05-09 18:44:00 -07:00
Marc Egli
d86dfb6336 Fix sonos sleep timer (#7503) 2017-05-09 18:35:51 +02:00
Josh Anderson
b34c58386c Correct retrieval of spotify shuffle state (#7505)
Returned on the current playback response itself, not the device
2017-05-09 17:35:30 +02:00
abmantis
5cb3382425 new source only forces "play" if the current state is "playing" (#7506) 2017-05-09 17:34:17 +02:00
Adam Mills
40d27cde0e Refactor sun component for correctness (#7295)
* Refactor sun component for correctness

* Convert datetimes to dates for astral

* Fix tests for updated code

* Fix times now that calcs are fixed

* Move sun functions to helpers

* Fix flake on new file

* Additional tweaks from review

* Update requirements
2017-05-09 00:03:34 -07:00
Oliver
419d97fc06 Fixed potential AttributeError when checking for deleted sources (#7502) 2017-05-09 07:24:18 +02:00
Andrey
1cd51bc6a8 Switch onkyo to pypi (#7497) 2017-05-09 08:13:29 +03:00
Fabian Affolter
c12c742297 Upgrade beautifulsoup4 to 4.6.0 (#7491) 2017-05-08 19:39:40 +02:00
Johan Bloemberg
ce879b7eb8 Prevent printing of packets. (#7492)
A small bug in the python-rflink library caused packets to be printed. This update prevents this from happening.
2017-05-08 17:04:17 +02:00
Fabian Affolter
d7e3962cc0 Upgrade async_timeout to 1.2.1 (#7490) 2017-05-08 17:02:37 +02:00
Anders Melchiorsen
86b34b40a1 LIFX: avoid out-of-bounds hue aborting the colorloop effect (#7495)
The hue is now a float but the hsbk conversion still believed it to be
an integer that could not be larger than 359. The float can in fact be,
for example, 359.9 and this would cause an out-of-bounds error in the
set_color call.

For completeness, the initial hue is also changed to a float.
2017-05-08 16:51:27 +02:00
Paulus Schoutsen
12293d6600 0.44.2 (#7488)
* Version bump to 0.44.2

* Update frontend
2017-05-07 22:07:52 -07:00
Mitesh Patel
66cbdc3043 Uses pypi for deps (#7485) 2017-05-07 17:32:13 -07:00
Paulus Schoutsen
e1d1385358 Fix travis 2017-05-07 16:55:22 -07:00
Paulus Schoutsen
bafc04ca42 Update tox.ini 2017-05-07 16:53:28 -07:00
Paulus Schoutsen
5717c87097 Update tox.ini 2017-05-07 16:51:46 -07:00
Paulus Schoutsen
ab9c394e93 Version bump to 0.44.1 2017-05-07 15:09:53 -07:00
Paulus Schoutsen
5d13f36a4b Merge pull request #7482 from home-assistant/release-0-44-1
0.44.1
2017-05-07 15:08:37 -07:00
Caleb
4165629f97 Update to pyunifi 2.12 (#7468)
* Update to pyunifi 2.12

* Update requirements_all.txt
2017-05-07 14:03:39 -07:00
Marc Egli
f87b9b7b85 Fix plant MIN_TEMPERATURE, MAX_TEMPERATURE validation (#7476)
* Fix plant MIN_TEMPERATURE, MAX_TEMPERATURE validation

small_float only allows values from 0 to 1 so we should use float instead

* Do not use vol.All for a single validation
2017-05-07 14:03:14 -07:00
Martin Hjelmare
2ab45441a8 Upgrade pymysensors to 0.10.0 (#7469) 2017-05-07 14:02:53 -07:00
pezinek
fcdfebefd9 Forecasts for weather underground (#7062) 2017-05-07 13:56:19 -07:00
Brian Cribbs
9b920b3b40 fixing nits 2017-05-07 13:56:05 -07:00
Brian Cribbs
0c94df9fcf fixing documentation 2017-05-07 13:56:05 -07:00
Brian Cribbs
1ba4435693 repairing functionality for non-zero based ranges 2017-05-07 13:56:05 -07:00
Paulus Schoutsen
00ec50da4b Update frontend 2017-05-07 13:50:07 -07:00
Marc Egli
c1056ea4d4 Fix plant MIN_TEMPERATURE, MAX_TEMPERATURE validation (#7476)
* Fix plant MIN_TEMPERATURE, MAX_TEMPERATURE validation

small_float only allows values from 0 to 1 so we should use float instead

* Do not use vol.All for a single validation
2017-05-07 15:15:18 +02:00
Paulus Schoutsen
9440ff881f Remove listening to homeassistant_start with event automation (#7474) 2017-05-06 23:52:39 -07:00
Robbie Trencheny
c525ee9daa Make this an error instead of an info 2017-05-06 23:11:11 -07:00
Paulus Schoutsen
79ca47640e Update requirements_test_all.txt 2017-05-06 23:02:12 -07:00
Caleb
41212b90c4 Update to pyunifi 2.12 (#7468)
* Update to pyunifi 2.12

* Update requirements_all.txt
2017-05-06 22:39:21 -07:00
Paulus Schoutsen
aa6339818e Test only dependencies (#7472)
* Generate requirements file for tests

* Update tox

* Update validate

* Lint

* Tweak order in travis.yml to run longest job first
2017-05-06 22:37:31 -07:00
Paulus Schoutsen
305309a59e Upgrade Dockerfile to Python 3.6 (#7471) 2017-05-06 20:16:40 -07:00
Paulus Schoutsen
ea095de98e Demo: Update old group member thermostat.ecobee -> climate 2017-05-06 19:40:59 -07:00
Paulus Schoutsen
e8a33758c1 Capitalize group names in demo 2017-05-06 19:38:48 -07:00
Martin Hjelmare
47034f83f4 Upgrade pymysensors to 0.10.0 (#7469) 2017-05-06 19:10:17 -07:00
Andrey
2c1df75c07 Switch russound, pymysensors, and pocketcasts to pypi (#7449)
* Switch russound to pypi

* Switch pymysensors to pypi

* Switch pocketcasts to pypi
2017-05-07 02:22:38 +02:00
pezinek
7a70496b11 Forecasts for weather underground (#7062) 2017-05-06 10:11:31 -07:00
Adam Mills
7dd7f509ca Add tests for deprecation helpers (#7452) 2017-05-06 10:10:48 -07:00
Robbie Trencheny
6cc85adb81 Merge pull request #7460 from home-assistant/fix/default-knx-port
Fix object type for default KNX port
2017-05-05 18:03:46 -07:00
Josh Wright
2971a24c56 Fix object type for default KNX port
#7429 describes a TypeError that is raised if the port is omitted in the config for the KNX component (integer is required (got type str)). This commit changes the default port from a string to an integer. I expect this will resolve that issue...
2017-05-05 19:19:24 -04:00
Nuno Sousa
20ded1ba3e Add datadog component (#7158)
* Add datadog component

* Improve test_invalid_config datadog test

* Use assert_setup_component for test setup
2017-05-06 00:34:40 +02:00
Josh Wright
2e4ae3e73d PyPI Openzwave (#7415)
* Remove default zwave config path

PYOZW now has much more comprehensive default handling for the config
path (in src-lib/libopenzwave/libopenzwave.pyx:getConfig()). It looks in
the same place we were looking, plus _many_ more. It will certainly do a
much better job of finding the config files than we will (and will be
updated as the library is changed, so we don't end up chasing it). The
getConfig() method has been there for a while, but was subsntially
improved recently.

This change simply leaves the config_path as None if it is not
specified, which will trigger the default handling in PYOZW.

* Install python-openzwave from PyPI

As of version 0.4, python-openzwave supports installation from PyPI,
which means we can use our 'normal' dependency management tooling to
install it. Yay.

This uses the default 'embed' build (which goes and downloads
statically sources to avoid having to compile anything locally). Check
out the python-openzwave readme for more details.

* Add python-openzwave deps to .travis.yml

Python OpenZwave require the libudev headers to build. This adds the
libudev-dev package to Travis runs via the 'apt' addon for Travis.

Thanks to @MartinHjelmare for this fix.

* Update docker build for PyPI openzwave

Now that PYOZW can be install from PyPI, the docker image build process
can be simplified to remove the explicit compilation of PYOZW.
2017-05-05 14:57:14 -07:00
Gergely Imreh
4b5be750b2 sensor.envirophat: add missing requirement (#7451)
Adding requirements that is not explicitly pulled in by the library
that manages the Enviro pHAT.
2017-05-05 11:37:54 -07:00
florincosta
92411cdc18 Add new raspihats component (#7392)
* Add new raspihats component

* added raspihats to COMMENT_REQUIREMENTS in gen_requirements_all.py

* disabled pylint import errors

* using hass.data for storing i2c-hats manager
2017-05-05 00:02:47 -07:00
Daniel Høyer Iversen
526abdd329 Add hass to rfxtrx object (#6844) 2017-05-04 23:50:53 -07:00
Paulus Schoutsen
61196b1c83 Version bump to 0.45.0.dev0 2017-05-04 21:41:32 -07:00
Paulus Schoutsen
629bf3eefd Update frontend 2017-05-04 21:38:28 -07:00
Paulus Schoutsen
3b237795ba Merge branch 'release-0-44' into dev 2017-05-04 21:23:40 -07:00
Robbie Trencheny
12910de9ae Merge pull request #7289 from jminardi/jminardi/tplink-logout
Log out of TP-Link router after devices are recorded.
2017-05-04 18:47:25 -07:00
Robbie Trencheny
2f686124c8 Merge pull request #7446 from amelchio/lifx-misc
LIFX: small error corrections
2017-05-04 18:45:14 -07:00
Robbie Trencheny
b59ca034ae Merge pull request #7393 from cribbstechnologies/dev
MQTT Cover: Fixed status reporting for range with non-zero base
2017-05-04 18:32:24 -07:00
Anders Melchiorsen
78a3f259d6 LIFX: handle unavailable lights gracefully
Recent aiolifx allow sending messages to unregistered devices (as a
no-op). This is handy because bulbs can disappear anytime we yield and
constantly testing for availability is both error-prone and annoying.

So keep the aiolifx device around until a new one registers on the same
mac_addr.
2017-05-04 22:51:00 +02:00
Anders Melchiorsen
494a776959 LIFX: avoid warnings about already running updates
Forcing a refresh will log a warning if the periodic async_update happens
to be running already.

So let's do the refresh locally and remove the force_refresh.
2017-05-04 00:21:24 +02:00
Anders Melchiorsen
193270c4fb LIFX: Update aiolifx requirement
This update silences some warnings (frawau/aiolifx#7).
2017-05-04 00:21:24 +02:00
Anders Melchiorsen
ec490070ca LIFX: Move random hue initial color to the LIFXEffect base class
It's a reasonable default for several light effects.
2017-05-04 00:21:24 +02:00
Anders Melchiorsen
8233f086cd LIFX: Use 3500K as neutral white
This does not really matter because the colorloop uses saturated colors
(without much white). Anyway, just copy the 3500K that the LIFX app uses.
2017-05-04 00:21:24 +02:00
Anders Melchiorsen
99e34539b9 LIFX: fix color restore after running effects
State restoration takes up to a second because bulbs can be slow to react.
During this time an effect could keep running, overwriting the state that we
were trying to restore.

Now the effect forgets the light immediately and it thus avoids further
changes while the restored state settles.
2017-05-04 00:21:24 +02:00
Anders Melchiorsen
71d909483c LIFX: refresh state after stopping an effect
This clears the internal cache in case polling picked up the state as set by
an effect.

For example, aborting an effect by selecting a new brightness could keep a
color set by the effect.
2017-05-04 00:21:24 +02:00
Brian Cribbs
1b2c83145c fixing nits 2017-05-02 15:41:45 -04:00
Brian Cribbs
098e28534b fixing documentation 2017-05-01 13:34:34 -04:00
Brian Cribbs
dc716cd971 repairing functionality for non-zero based ranges 2017-05-01 13:22:54 -04:00
Jack Minardi
7a24e210ae Try again to pass string to error msg 2017-05-01 09:31:23 -04:00
Jack Minardi
bc0559813c Dont add two strings inside logger call 2017-04-30 22:26:16 -04:00
Jack Minardi
dd7690f265 Use % formatting 2017-04-30 21:31:55 -04:00
Jack Minardi
b6827ce57a Use throwaray variable name 2017-04-30 21:02:03 -04:00
Jack Minardi
8bf1c21738 Add space 2017-04-25 00:48:23 -04:00
Jack Minardi
943861a8a3 Remove unused var 2017-04-25 00:45:59 -04:00
Jack Minardi
450fd7f2b5 Log out of router admin interface after devices are recorded. 2017-04-25 00:34:26 -04:00
Jack Minardi
2d5da3e958 Catch KeyError; Add response.text to error message 2017-04-25 00:32:31 -04:00
352 changed files with 13777 additions and 4688 deletions

View File

@@ -20,6 +20,12 @@ omit =
homeassistant/components/android_ip_webcam.py
homeassistant/components/*/android_ip_webcam.py
homeassistant/components/arlo.py
homeassistant/components/*/arlo.py
homeassistant/components/axis.py
homeassistant/components/*/axis.py
homeassistant/components/bbb_gpio.py
homeassistant/components/*/bbb_gpio.py
@@ -59,6 +65,9 @@ omit =
homeassistant/components/isy994.py
homeassistant/components/*/isy994.py
homeassistant/components/kira.py
homeassistant/components/*/kira.py
homeassistant/components/lutron.py
homeassistant/components/*/lutron.py
@@ -83,12 +92,21 @@ omit =
homeassistant/components/qwikswitch.py
homeassistant/components/*/qwikswitch.py
homeassistant/components/rachio.py
homeassistant/components/*/rachio.py
homeassistant/components/raspihats.py
homeassistant/components/*/raspihats.py
homeassistant/components/rfxtrx.py
homeassistant/components/*/rfxtrx.py
homeassistant/components/rpi_gpio.py
homeassistant/components/*/rpi_gpio.py
homeassistant/components/rpi_pfio.py
homeassistant/components/*/rpi_pfio.py
homeassistant/components/scsgate.py
homeassistant/components/*/scsgate.py
@@ -175,6 +193,7 @@ omit =
homeassistant/components/binary_sensor/flic.py
homeassistant/components/binary_sensor/hikvision.py
homeassistant/components/binary_sensor/iss.py
homeassistant/components/binary_sensor/mystrom.py
homeassistant/components/binary_sensor/pilight.py
homeassistant/components/binary_sensor/ping.py
homeassistant/components/binary_sensor/rest.py
@@ -239,6 +258,7 @@ omit =
homeassistant/components/ifttt.py
homeassistant/components/image_processing/dlib_face_detect.py
homeassistant/components/image_processing/dlib_face_identify.py
homeassistant/components/image_processing/seven_segments.py
homeassistant/components/joaoapps_join.py
homeassistant/components/keyboard.py
homeassistant/components/keyboard_remote.py

View File

@@ -1,13 +1,15 @@
sudo: false
addons:
apt:
packages:
- libudev-dev
matrix:
fast_finish: true
include:
- python: "3.4.2"
env: TOXENV=py34
- python: "3.4.2"
env: TOXENV=requirements
- python: "3.4.2"
env: TOXENV=lint
- python: "3.4.2"
env: TOXENV=py34
# - python: "3.5"
# env: TOXENV=typing
- python: "3.5"
@@ -16,6 +18,8 @@ matrix:
env: TOXENV=py36
- python: "3.6-dev"
env: TOXENV=py36
- python: "3.4.2"
env: TOXENV=requirements
# allow_failures:
# - python: "3.5"
# env: TOXENV=typing
@@ -25,5 +29,5 @@ cache:
- $HOME/.cache/pip
install: pip install -U tox coveralls
language: python
script: tox
script: travis_wait tox
after_success: coveralls

View File

@@ -1,11 +1,10 @@
FROM python:3.5
FROM python:3.6
MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
# Uncomment any of the following lines to disable the installation.
#ENV INSTALL_TELLSTICK no
#ENV INSTALL_OPENALPR no
#ENV INSTALL_FFMPEG no
#ENV INSTALL_OPENZWAVE no
#ENV INSTALL_LIBCEC no
#ENV INSTALL_PHANTOMJS no
#ENV INSTALL_COAP_CLIENT no

View File

@@ -1,8 +1,6 @@
<ul>
<li><a href="https://community.home-assistant.io">📌 Community Forums</a></li>
<li><a href="https://github.com/home-assistant/home-assistant">🚀 GitHub</a></li>
<li><a href="https://home-assistant.io/">🏡 Homepage</a></li>
<li><a href="https://gitter.im/home-assistant/home-assistant">💬 Gitter</a></li>
<li><a href="https://pypi.python.org/pypi/homeassistant">💾 Download Releases</a></li>
<li><a href="https://home-assistant.io/">Homepage</a></li>
<li><a href="https://community.home-assistant.io">Community Forums</a></li>
<li><a href="https://github.com/home-assistant/home-assistant">GitHub</a></li>
<li><a href="https://gitter.im/home-assistant/home-assistant">Gitter</a></li>
</ul>
<hr>

View File

@@ -10,6 +10,7 @@ import threading
from typing import Optional, List
from homeassistant import monkey_patch
from homeassistant.const import (
__version__,
EVENT_HOMEASSISTANT_START,
@@ -17,7 +18,6 @@ from homeassistant.const import (
REQUIRED_PYTHON_VER_WIN,
RESTART_EXIT_CODE,
)
from homeassistant.util.async import run_callback_threadsafe
def attempt_use_uvloop():
@@ -310,6 +310,9 @@ def setup_and_run_hass(config_dir: str,
return None
if args.open_ui:
# Imported here to avoid importing asyncio before monkey patch
from homeassistant.util.async import run_callback_threadsafe
def open_browser(event):
"""Open the webinterface in a browser."""
if hass.config.api is not None:
@@ -371,6 +374,13 @@ def main() -> int:
"""Start Home Assistant."""
validate_python()
if os.environ.get('HASS_MONKEYPATCH_ASYNCIO') == '1':
if sys.version_info[:3] >= (3, 6):
monkey_patch.disable_c_asyncio()
monkey_patch.patch_weakref_tasks()
elif sys.version_info[:3] < (3, 5, 3):
monkey_patch.patch_weakref_tasks()
attempt_use_uvloop()
if sys.version_info[:3] < (3, 5, 3):

View File

@@ -83,8 +83,7 @@ def async_from_config_dict(config: Dict[str, Any],
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
return None
yield from hass.loop.run_in_executor(
None, conf_util.process_ha_config_upgrade, hass)
yield from hass.async_add_job(conf_util.process_ha_config_upgrade, hass)
if enable_log:
async_enable_logging(hass, verbose, log_rotate_days)
@@ -95,7 +94,7 @@ def async_from_config_dict(config: Dict[str, Any],
'This may cause issues.')
if not loader.PREPARED:
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
yield from hass.async_add_job(loader.prepare, hass)
# Merge packages
conf_util.merge_packages_config(
@@ -184,14 +183,13 @@ def async_from_config_file(config_path: str,
# Set config dir to directory holding config file
config_dir = os.path.abspath(os.path.dirname(config_path))
hass.config.config_dir = config_dir
yield from hass.loop.run_in_executor(
None, mount_local_lib_path, config_dir)
yield from hass.async_add_job(mount_local_lib_path, config_dir)
async_enable_logging(hass, verbose, log_rotate_days)
try:
config_dict = yield from hass.loop.run_in_executor(
None, conf_util.load_yaml_config_file, config_path)
config_dict = yield from hass.async_add_job(
conf_util.load_yaml_config_file, config_path)
except HomeAssistantError as err:
_LOGGER.error('Error loading %s: %s', config_path, err)
return None

View File

@@ -123,8 +123,8 @@ def async_setup(hass, config):
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
for service in SERVICE_TO_METHOD:
@@ -158,8 +158,7 @@ class AlarmControlPanel(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.alarm_disarm, code)
return self.hass.async_add_job(self.alarm_disarm, code)
def alarm_arm_home(self, code=None):
"""Send arm home command."""
@@ -170,8 +169,7 @@ class AlarmControlPanel(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.alarm_arm_home, code)
return self.hass.async_add_job(self.alarm_arm_home, code)
def alarm_arm_away(self, code=None):
"""Send arm away command."""
@@ -182,8 +180,7 @@ class AlarmControlPanel(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.alarm_arm_away, code)
return self.hass.async_add_job(self.alarm_arm_away, code)
def alarm_trigger(self, code=None):
"""Send alarm trigger command."""
@@ -194,8 +191,7 @@ class AlarmControlPanel(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.alarm_trigger, code)
return self.hass.async_add_job(self.alarm_trigger, code)
@property
def state_attributes(self):

View File

@@ -117,7 +117,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
def alarm_arm_home(self, code=None):
"""Send arm home command."""
self._alarm.arm('home')
self._alarm.arm('stay')
def alarm_arm_away(self, code=None):
"""Send arm away command."""

View File

@@ -70,8 +70,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
device.async_alarm_keypress(keypress)
# Register Envisalink specific services
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
hass.services.async_register(

View File

@@ -4,6 +4,7 @@ Interfaces with Wink Cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.wink/
"""
import asyncio
import logging
import homeassistant.components.alarm_control_panel as alarm
@@ -42,6 +43,11 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
"""Initialize the Wink alarm."""
super().__init__(wink, hass)
@asyncio.coroutine
def async_added_to_hass(self):
"""Callback when entity is added to hass."""
self.hass.data[DOMAIN]['entities']['alarm_control_panel'].append(self)
@property
def state(self):
"""Return the state of the device."""

View File

@@ -128,8 +128,8 @@ def async_setup(hass, config):
all_alerts[entity.entity_id] = entity
# Read descriptions
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
descriptions = descriptions.get(DOMAIN, {})

View File

@@ -17,7 +17,6 @@ from homeassistant.core import callback
from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import template, script, config_validation as cv
from homeassistant.components.http import HomeAssistantView
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -36,7 +35,6 @@ CONF_TEXT = 'text'
CONF_FLASH_BRIEFINGS = 'flash_briefings'
CONF_UID = 'uid'
CONF_DATE = 'date'
CONF_TITLE = 'title'
CONF_AUDIO = 'audio'
CONF_TEXT = 'text'
@@ -88,7 +86,6 @@ CONFIG_SCHEMA = vol.Schema({
CONF_FLASH_BRIEFINGS: {
cv.string: vol.All(cv.ensure_list, [{
vol.Required(CONF_UID, default=str(uuid.uuid4())): cv.string,
vol.Optional(CONF_DATE, default=datetime.utcnow()): cv.string,
vol.Required(CONF_TITLE): cv.template,
vol.Optional(CONF_AUDIO): cv.template,
vol.Required(CONF_TEXT, default=""): cv.template,
@@ -331,10 +328,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
else:
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)
if isinstance(item[CONF_DATE], str):
item[CONF_DATE] = dt_util.parse_datetime(item[CONF_DATE])
output[ATTR_UPDATE_DATE] = item[CONF_DATE].strftime(DATE_FORMAT)
output[ATTR_UPDATE_DATE] = datetime.now().strftime(DATE_FORMAT)
briefing.append(output)

View File

@@ -13,7 +13,7 @@ from homeassistant.const import (CONF_HOST, CONF_PORT)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
REQUIREMENTS = ['apcaccess==0.0.4']
REQUIREMENTS = ['apcaccess==0.0.10']
_LOGGER = logging.getLogger(__name__)

View File

@@ -83,7 +83,7 @@ class APIEventStream(HomeAssistantView):
stop_obj = object()
to_write = asyncio.Queue(loop=hass.loop)
restrict = request.GET.get('restrict')
restrict = request.query.get('restrict')
if restrict:
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]

View File

@@ -0,0 +1,60 @@
"""
This component provides basic support for Netgear Arlo IP cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/arlo/
"""
import logging
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
import homeassistant.loader as loader
from requests.exceptions import HTTPError, ConnectTimeout
REQUIREMENTS = ['pyarlo==0.0.4']
_LOGGER = logging.getLogger(__name__)
CONF_ATTRIBUTION = 'Data provided by arlo.netgear.com'
DOMAIN = 'arlo'
DEFAULT_BRAND = 'Netgear Arlo'
NOTIFICATION_ID = 'arlo_notification'
NOTIFICATION_TITLE = 'Arlo Camera Setup'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up an Arlo component."""
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
persistent_notification = loader.get_component('persistent_notification')
try:
from pyarlo import PyArlo
arlo = PyArlo(username, password, preload=False)
if not arlo.is_connected:
return False
hass.data['arlo'] = arlo
except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Netgar Arlo: %s", str(ex))
persistent_notification.create(
hass, 'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
return True

View File

@@ -16,7 +16,7 @@ from homeassistant.core import CoreState
from homeassistant import config as conf_util
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START)
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID)
from homeassistant.components import logbook
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import extract_domain_configs, script, condition
@@ -26,6 +26,7 @@ from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.loader import get_platform
from homeassistant.util.dt import utcnow
import homeassistant.helpers.config_validation as cv
from homeassistant.components.frontend import register_built_in_panel
DOMAIN = 'automation'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
@@ -81,6 +82,8 @@ _TRIGGER_SCHEMA = vol.All(
_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
PLATFORM_SCHEMA = vol.Schema({
# str on purpose
CONF_ID: str,
CONF_ALIAS: cv.string,
vol.Optional(CONF_INITIAL_STATE): cv.boolean,
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
@@ -139,19 +142,24 @@ def reload(hass):
hass.services.call(DOMAIN, SERVICE_RELOAD)
def async_reload(hass):
"""Reload the automation from config.
Returns a coroutine object.
"""
return hass.services.async_call(DOMAIN, SERVICE_RELOAD)
@asyncio.coroutine
def async_setup(hass, config):
"""Set up the automation."""
component = EntityComponent(_LOGGER, DOMAIN, hass,
group_name=GROUP_NAME_ALL_AUTOMATIONS)
success = yield from _async_process_config(hass, config, component)
yield from _async_process_config(hass, config, component)
if not success:
return False
descriptions = yield from hass.loop.run_in_executor(
None, conf_util.load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
conf_util.load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml')
)
@@ -215,15 +223,20 @@ def async_setup(hass, config):
DOMAIN, service, turn_onoff_service_handler,
descriptions.get(service), schema=SERVICE_SCHEMA)
if 'frontend' in hass.config.components:
register_built_in_panel(hass, 'automation', 'Automations',
'mdi:playlist-play')
return True
class AutomationEntity(ToggleEntity):
"""Entity to show status of entity."""
def __init__(self, name, async_attach_triggers, cond_func, async_action,
hidden, initial_state):
def __init__(self, automation_id, name, async_attach_triggers, cond_func,
async_action, hidden, initial_state):
"""Initialize an automation entity."""
self._id = automation_id
self._name = name
self._async_attach_triggers = async_attach_triggers
self._async_detach_triggers = None
@@ -346,6 +359,16 @@ class AutomationEntity(ToggleEntity):
self.async_trigger)
yield from self.async_update_ha_state()
@property
def device_state_attributes(self):
"""Return automation attributes."""
if self._id is None:
return None
return {
CONF_ID: self._id
}
@asyncio.coroutine
def _async_process_config(hass, config, component):
@@ -359,6 +382,7 @@ def _async_process_config(hass, config, component):
conf = config[config_key]
for list_no, config_block in enumerate(conf):
automation_id = config_block.get(CONF_ID)
name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key,
list_no)
@@ -383,16 +407,14 @@ def _async_process_config(hass, config, component):
config_block.get(CONF_TRIGGER, []), name
)
entity = AutomationEntity(
name, async_attach_triggers, cond_func, action, hidden,
initial_state)
automation_id, name, async_attach_triggers, cond_func, action,
hidden, initial_state)
entities.append(entity)
if entities:
yield from component.async_add_entities(entities)
return len(entities) > 0
def _async_get_action(hass, config, name):
"""Return an action based on a configuration."""

View File

@@ -9,8 +9,8 @@ import logging
import voluptuous as vol
from homeassistant.core import callback, CoreState
from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_START
from homeassistant.core import callback
from homeassistant.const import CONF_PLATFORM
from homeassistant.helpers import config_validation as cv
CONF_EVENT_TYPE = 'event_type'
@@ -31,19 +31,6 @@ def async_trigger(hass, config, action):
event_type = config.get(CONF_EVENT_TYPE)
event_data = config.get(CONF_EVENT_DATA)
if (event_type == EVENT_HOMEASSISTANT_START and
hass.state == CoreState.starting):
_LOGGER.warning('Deprecation: Automations should not listen to event '
"'homeassistant_start'. Use platform 'homeassistant' "
'instead. Feature will be removed in 0.45')
hass.async_run_job(action, {
'trigger': {
'platform': 'event',
'event': None,
},
})
return lambda: None
@callback
def handle_event(event):
"""Listen for events and calls the action when data matches."""

View File

@@ -12,6 +12,7 @@ import homeassistant.util.dt as dt_util
from homeassistant.const import MATCH_ALL, CONF_PLATFORM
from homeassistant.helpers.event import (
async_track_state_change, async_track_point_in_utc_time)
from homeassistant.helpers.deprecation import get_deprecated
import homeassistant.helpers.config_validation as cv
CONF_ENTITY_ID = 'entity_id'
@@ -40,10 +41,11 @@ def async_trigger(hass, config, action):
"""Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID)
from_state = config.get(CONF_FROM, MATCH_ALL)
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
to_state = get_deprecated(config, CONF_TO, CONF_STATE, MATCH_ALL)
time_delta = config.get(CONF_FOR)
async_remove_state_for_cancel = None
async_remove_state_for_listener = None
match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL)
@callback
def clear_listener():
@@ -75,6 +77,11 @@ def async_trigger(hass, config, action):
}
})
# Ignore changes to state attributes if from/to is in use
if (not match_all and from_s is not None and to_s is not None and
from_s.last_changed == to_s.last_changed):
return
if time_delta is None:
call_action()
return

View File

@@ -16,8 +16,6 @@ from homeassistant.const import (
from homeassistant.helpers.event import async_track_sunrise, async_track_sunset
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['sun']
_LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.Schema({

View File

@@ -10,7 +10,7 @@ import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import CONF_AFTER, CONF_PLATFORM
from homeassistant.const import CONF_AT, CONF_PLATFORM, CONF_AFTER
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import async_track_time_change
@@ -22,20 +22,26 @@ _LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_PLATFORM): 'time',
CONF_AT: cv.time,
CONF_AFTER: cv.time,
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)),
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES,
CONF_SECONDS, CONF_AFTER))
CONF_SECONDS, CONF_AT, CONF_AFTER))
@asyncio.coroutine
def async_trigger(hass, config, action):
"""Listen for state changes based on configuration."""
if CONF_AFTER in config:
after = config.get(CONF_AFTER)
hours, minutes, seconds = after.hour, after.minute, after.second
if CONF_AT in config:
at_time = config.get(CONF_AT)
hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second
elif CONF_AFTER in config:
_LOGGER.warning("'after' is deprecated for the time trigger. Please "
"rename 'after' to 'at' in your configuration file.")
at_time = config.get(CONF_AFTER)
hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second
else:
hours = config.get(CONF_HOURS)
minutes = config.get(CONF_MINUTES)

View File

@@ -0,0 +1,314 @@
"""
Support for Axis devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/axis/
"""
import json
import logging
import os
import voluptuous as vol
from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
CONF_HOST, CONF_INCLUDE, CONF_NAME,
CONF_PASSWORD, CONF_TRIGGER_TIME,
CONF_USERNAME, EVENT_HOMEASSISTANT_STOP)
from homeassistant.components.discovery import SERVICE_AXIS
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
REQUIREMENTS = ['axis==7']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'axis'
CONFIG_FILE = 'axis.conf'
AXIS_DEVICES = {}
EVENT_TYPES = ['motion', 'vmd3', 'pir', 'sound',
'daynight', 'tampering', 'input']
PLATFORMS = ['camera']
AXIS_INCLUDE = EVENT_TYPES + PLATFORMS
AXIS_DEFAULT_HOST = '192.168.0.90'
AXIS_DEFAULT_USERNAME = 'root'
AXIS_DEFAULT_PASSWORD = 'pass'
DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_INCLUDE):
vol.All(cv.ensure_list, [vol.In(AXIS_INCLUDE)]),
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_HOST, default=AXIS_DEFAULT_HOST): cv.string,
vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_TRIGGER_TIME, default=0): cv.positive_int,
vol.Optional(ATTR_LOCATION, default=''): cv.string,
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: DEVICE_SCHEMA,
}),
}, extra=vol.ALLOW_EXTRA)
def request_configuration(hass, name, host, serialnumber):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
def configuration_callback(callback_data):
"""Called when config is submitted."""
if CONF_INCLUDE not in callback_data:
configurator.notify_errors(request_id,
"Functionality mandatory.")
return False
callback_data[CONF_INCLUDE] = callback_data[CONF_INCLUDE].split()
callback_data[CONF_HOST] = host
if CONF_NAME not in callback_data:
callback_data[CONF_NAME] = name
try:
config = DEVICE_SCHEMA(callback_data)
except vol.Invalid:
configurator.notify_errors(request_id,
"Bad input, please check spelling.")
return False
if setup_device(hass, config):
config_file = _read_config(hass)
config_file[serialnumber] = dict(config)
del config_file[serialnumber]['hass']
_write_config(hass, config_file)
configurator.request_done(request_id)
else:
configurator.notify_errors(request_id,
"Failed to register, please try again.")
return False
title = '{} ({})'.format(name, host)
request_id = configurator.request_config(
hass, title, configuration_callback,
description='Functionality: ' + str(AXIS_INCLUDE),
entity_picture="/static/images/logo_axis.png",
link_name='Axis platform documentation',
link_url='https://home-assistant.io/components/axis/',
submit_caption="Confirm",
fields=[
{'id': CONF_NAME,
'name': "Device name",
'type': 'text'},
{'id': CONF_USERNAME,
'name': "User name",
'type': 'text'},
{'id': CONF_PASSWORD,
'name': 'Password',
'type': 'password'},
{'id': CONF_INCLUDE,
'name': "Device functionality (space separated list)",
'type': 'text'},
{'id': ATTR_LOCATION,
'name': "Physical location of device (optional)",
'type': 'text'},
{'id': CONF_TRIGGER_TIME,
'name': "Sensor update interval (optional)",
'type': 'number'},
]
)
def setup(hass, base_config):
"""Common setup for Axis devices."""
def _shutdown(call): # pylint: disable=unused-argument
"""Stop the metadatastream on shutdown."""
for serialnumber, device in AXIS_DEVICES.items():
_LOGGER.info("Stopping metadatastream for %s.", serialnumber)
device.stop_metadatastream()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
def axis_device_discovered(service, discovery_info):
"""Called when axis devices has been found."""
host = discovery_info['host']
name = discovery_info['hostname']
serialnumber = discovery_info['properties']['macaddress']
if serialnumber not in AXIS_DEVICES:
config_file = _read_config(hass)
if serialnumber in config_file:
try:
config = DEVICE_SCHEMA(config_file[serialnumber])
except vol.Invalid as err:
_LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err)
return False
if not setup_device(hass, config):
_LOGGER.error("Couldn\'t set up %s", config['name'])
else:
request_configuration(hass, name, host, serialnumber)
discovery.listen(hass, SERVICE_AXIS, axis_device_discovered)
if DOMAIN in base_config:
for device in base_config[DOMAIN]:
config = base_config[DOMAIN][device]
if CONF_NAME not in config:
config[CONF_NAME] = device
if not setup_device(hass, config):
_LOGGER.error("Couldn\'t set up %s", config['name'])
return True
def setup_device(hass, config):
"""Set up device."""
from axis import AxisDevice
config['hass'] = hass
device = AxisDevice(config) # Initialize device
enable_metadatastream = False
if device.serial_number is None:
# If there is no serial number a connection could not be made
_LOGGER.error("Couldn\'t connect to %s", config[CONF_HOST])
return False
for component in config[CONF_INCLUDE]:
if component in EVENT_TYPES:
# Sensors are created by device calling event_initialized
# when receiving initialize messages on metadatastream
device.add_event_topic(convert(component, 'type', 'subscribe'))
if not enable_metadatastream:
enable_metadatastream = True
else:
discovery.load_platform(hass, component, DOMAIN, config)
if enable_metadatastream:
device.initialize_new_event = event_initialized
device.initiate_metadatastream()
AXIS_DEVICES[device.serial_number] = device
return True
def _read_config(hass):
"""Read Axis config."""
path = hass.config.path(CONFIG_FILE)
if not os.path.isfile(path):
return {}
with open(path) as f_handle:
# Guard against empty file
return json.loads(f_handle.read() or '{}')
def _write_config(hass, config):
"""Write Axis config."""
data = json.dumps(config)
with open(hass.config.path(CONFIG_FILE), 'w', encoding='utf-8') as outfile:
outfile.write(data)
def event_initialized(event):
"""Register event initialized on metadatastream here."""
hass = event.device_config('hass')
discovery.load_platform(hass,
convert(event.topic, 'topic', 'platform'),
DOMAIN, {'axis_event': event})
class AxisDeviceEvent(Entity):
"""Representation of a Axis device event."""
def __init__(self, axis_event):
"""Initialize the event."""
self.axis_event = axis_event
self._event_class = convert(self.axis_event.topic, 'topic', 'class')
self._name = '{}_{}_{}'.format(self.axis_event.device_name,
convert(self.axis_event.topic,
'topic', 'type'),
self.axis_event.id)
self.axis_event.callback = self._update_callback
def _update_callback(self):
"""Update the sensor's state, if needed."""
self.update()
self.schedule_update_ha_state()
@property
def name(self):
"""Return the name of the event."""
return self._name
@property
def device_class(self):
"""Return the class of the event."""
return self._event_class
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def device_state_attributes(self):
"""Return the state attributes of the event."""
attr = {}
tripped = self.axis_event.is_tripped
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
location = self.axis_event.device_config(ATTR_LOCATION)
if location:
attr[ATTR_LOCATION] = location
return attr
def convert(item, from_key, to_key):
"""Translate between Axis and HASS syntax."""
for entry in REMAP:
if entry[from_key] == item:
return entry[to_key]
REMAP = [{'type': 'motion',
'class': 'motion',
'topic': 'tns1:VideoAnalytics/tnsaxis:MotionDetection',
'subscribe': 'onvif:VideoAnalytics/axis:MotionDetection',
'platform': 'binary_sensor'},
{'type': 'vmd3',
'class': 'motion',
'topic': 'tns1:RuleEngine/tnsaxis:VMD3/vmd3_video_1',
'subscribe': 'onvif:RuleEngine/axis:VMD3/vmd3_video_1',
'platform': 'binary_sensor'},
{'type': 'pir',
'class': 'motion',
'topic': 'tns1:Device/tnsaxis:Sensor/PIR',
'subscribe': 'onvif:Device/axis:Sensor/axis:PIR',
'platform': 'binary_sensor'},
{'type': 'sound',
'class': 'sound',
'topic': 'tns1:AudioSource/tnsaxis:TriggerLevel',
'subscribe': 'onvif:AudioSource/axis:TriggerLevel',
'platform': 'binary_sensor'},
{'type': 'daynight',
'class': 'light',
'topic': 'tns1:VideoSource/tnsaxis:DayNightVision',
'subscribe': 'onvif:VideoSource/axis:DayNightVision',
'platform': 'binary_sensor'},
{'type': 'tampering',
'class': 'safety',
'topic': 'tns1:VideoSource/tnsaxis:Tampering',
'subscribe': 'onvif:VideoSource/axis:Tampering',
'platform': 'binary_sensor'},
{'type': 'input',
'class': 'input',
'topic': 'tns1:Device/tnsaxis:IO/Port',
'subscribe': 'onvif:Device/axis:IO/Port',
'platform': 'sensor'}, ]

View File

@@ -0,0 +1,68 @@
"""
Support for Axis binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.axis/
"""
import logging
from datetime import timedelta
from homeassistant.components.binary_sensor import (BinarySensorDevice)
from homeassistant.components.axis import (AxisDeviceEvent)
from homeassistant.const import (CONF_TRIGGER_TIME)
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.util.dt import utcnow
DEPENDENCIES = ['axis']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Axis device event."""
add_devices([AxisBinarySensor(discovery_info['axis_event'], hass)], True)
class AxisBinarySensor(AxisDeviceEvent, BinarySensorDevice):
"""Representation of a binary Axis event."""
def __init__(self, axis_event, hass):
"""Initialize the binary sensor."""
self.hass = hass
self._state = False
self._delay = axis_event.device_config(CONF_TRIGGER_TIME)
self._timer = None
AxisDeviceEvent.__init__(self, axis_event)
@property
def is_on(self):
"""Return true if event is active."""
return self._state
def update(self):
"""Get the latest data and update the state."""
self._state = self.axis_event.is_tripped
def _update_callback(self):
"""Update the sensor's state, if needed."""
self.update()
if self._timer is not None:
self._timer()
self._timer = None
if self._delay > 0 and not self.is_on:
# Set timer to wait until updating the state
def _delay_update(now):
"""Timer callback for sensor update."""
_LOGGER.debug("%s Called delayed (%s sec) update.",
self._name, self._delay)
self.schedule_update_ha_state()
self._timer = None
self._timer = track_point_in_utc_time(
self.hass, _delay_update,
utcnow() + timedelta(seconds=self._delay))
else:
self.schedule_update_ha_state()

View File

@@ -0,0 +1,95 @@
"""
Support for the myStrom buttons.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.mystrom/
"""
import asyncio
import logging
from homeassistant.components.binary_sensor import (BinarySensorDevice, DOMAIN)
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http']
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up myStrom Binary Sensor."""
hass.http.register_view(MyStromView(async_add_devices))
return True
class MyStromView(HomeAssistantView):
"""View to handle requests from myStrom buttons."""
url = '/api/mystrom'
name = 'api:mystrom'
def __init__(self, add_devices):
"""Initialize the myStrom URL endpoint."""
self.buttons = {}
self.add_devices = add_devices
@asyncio.coroutine
def get(self, request):
"""The GET request received from a myStrom button."""
res = yield from self._handle(request.app['hass'], request.query)
return res
@asyncio.coroutine
def _handle(self, hass, data):
"""Handle requests to the myStrom endpoint."""
button_action = list(data.keys())[0]
button_id = data[button_action]
entity_id = '{}.{}_{}'.format(DOMAIN, button_id, button_action)
if button_action not in ['single', 'double', 'long', 'touch']:
_LOGGER.error(
"Received unidentified message from myStrom button: %s", data)
return ("Received unidentified message: {}".format(data),
HTTP_UNPROCESSABLE_ENTITY)
if entity_id not in self.buttons:
_LOGGER.info("New myStrom button/action detected: %s/%s",
button_id, button_action)
self.buttons[entity_id] = MyStromBinarySensor(
'{}_{}'.format(button_id, button_action))
hass.async_add_job(self.add_devices, [self.buttons[entity_id]])
else:
new_state = True if self.buttons[entity_id].state == 'off' \
else False
self.buttons[entity_id].async_on_update(new_state)
class MyStromBinarySensor(BinarySensorDevice):
"""Representation of a myStrom button."""
def __init__(self, button_id):
"""Initialize the myStrom Binary sensor."""
self._button_id = button_id
self._state = None
@property
def name(self):
"""Return the name of the sensor."""
return self._button_id
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self._state
def async_on_update(self, value):
"""Receive an update."""
self._state = value
self.hass.async_add_job(self.async_update_ha_state())

View File

@@ -0,0 +1,131 @@
"""
Configure a binary_sensor using a digital input from a raspihats board.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.raspihats/
"""
import logging
import voluptuous as vol
from homeassistant.const import (
CONF_NAME, CONF_DEVICE_CLASS, DEVICE_DEFAULT_NAME
)
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
PLATFORM_SCHEMA, BinarySensorDevice
)
from homeassistant.components.raspihats import (
CONF_I2C_HATS, CONF_BOARD, CONF_ADDRESS, CONF_CHANNELS, CONF_INDEX,
CONF_INVERT_LOGIC, I2C_HAT_NAMES, I2C_HATS_MANAGER, I2CHatsException
)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['raspihats']
DEFAULT_INVERT_LOGIC = False
DEFAULT_DEVICE_CLASS = None
_CHANNELS_SCHEMA = vol.Schema([{
vol.Required(CONF_INDEX): cv.positive_int,
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean,
vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): cv.string,
}])
_I2C_HATS_SCHEMA = vol.Schema([{
vol.Required(CONF_BOARD): vol.In(I2C_HAT_NAMES),
vol.Required(CONF_ADDRESS): vol.Coerce(int),
vol.Required(CONF_CHANNELS): _CHANNELS_SCHEMA
}])
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_I2C_HATS): _I2C_HATS_SCHEMA,
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the raspihats binary_sensor devices."""
I2CHatBinarySensor.I2C_HATS_MANAGER = hass.data[I2C_HATS_MANAGER]
binary_sensors = []
i2c_hat_configs = config.get(CONF_I2C_HATS)
for i2c_hat_config in i2c_hat_configs:
address = i2c_hat_config[CONF_ADDRESS]
board = i2c_hat_config[CONF_BOARD]
try:
I2CHatBinarySensor.I2C_HATS_MANAGER.register_board(board, address)
for channel_config in i2c_hat_config[CONF_CHANNELS]:
binary_sensors.append(
I2CHatBinarySensor(
address,
channel_config[CONF_INDEX],
channel_config[CONF_NAME],
channel_config[CONF_INVERT_LOGIC],
channel_config[CONF_DEVICE_CLASS]
)
)
except I2CHatsException as ex:
_LOGGER.error(
"Failed to register " + board + "I2CHat@" + hex(address) + " "
+ str(ex)
)
add_devices(binary_sensors)
class I2CHatBinarySensor(BinarySensorDevice):
"""Represents a binary sensor that uses a I2C-HAT digital input."""
I2C_HATS_MANAGER = None
def __init__(self, address, channel, name, invert_logic, device_class):
"""Initialize sensor."""
self._address = address
self._channel = channel
self._name = name or DEVICE_DEFAULT_NAME
self._invert_logic = invert_logic
self._device_class = device_class
self._state = self.I2C_HATS_MANAGER.read_di(
self._address,
self._channel
)
def online_callback():
"""Callback fired when board is online."""
self.schedule_update_ha_state()
self.I2C_HATS_MANAGER.register_online_callback(
self._address,
self._channel,
online_callback
)
def edge_callback(state):
"""Read digital input state."""
self._state = state
self.schedule_update_ha_state()
self.I2C_HATS_MANAGER.register_di_callback(
self._address,
self._channel,
edge_callback
)
@property
def device_class(self):
"""Return the class of this sensor."""
return self._device_class
@property
def name(self):
"""Return the name of this sensor."""
return self._name
@property
def should_poll(self):
"""Polling not needed for this sensor."""
return False
@property
def is_on(self):
"""Return the state of this sensor."""
return self._state != self._invert_logic

View File

@@ -0,0 +1,93 @@
"""
Support for binary sensor using the PiFace Digital I/O module on a RPi.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.rpi_pfio/
"""
import logging
import voluptuous as vol
import homeassistant.components.rpi_pfio as rpi_pfio
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import DEVICE_DEFAULT_NAME
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
ATTR_NAME = 'name'
ATTR_INVERT_LOGIC = 'invert_logic'
ATTR_SETTLE_TIME = 'settle_time'
CONF_PORTS = 'ports'
DEFAULT_INVERT_LOGIC = False
DEFAULT_SETTLE_TIME = 20
DEPENDENCIES = ['rpi_pfio']
PORT_SCHEMA = vol.Schema({
vol.Optional(ATTR_NAME, default=None): cv.string,
vol.Optional(ATTR_SETTLE_TIME, default=DEFAULT_SETTLE_TIME):
cv.positive_int,
vol.Optional(ATTR_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PORTS, default={}): vol.Schema({
cv.positive_int: PORT_SCHEMA
})
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the PiFace Digital Input devices."""
binary_sensors = []
ports = config.get('ports')
for port, port_entity in ports.items():
name = port_entity[ATTR_NAME]
settle_time = port_entity[ATTR_SETTLE_TIME] / 1000
invert_logic = port_entity[ATTR_INVERT_LOGIC]
binary_sensors.append(RPiPFIOBinarySensor(
hass, port, name, settle_time, invert_logic))
add_devices(binary_sensors, True)
rpi_pfio.activate_listener(hass)
class RPiPFIOBinarySensor(BinarySensorDevice):
"""Represent a binary sensor that a PiFace Digital Input."""
def __init__(self, hass, port, name, settle_time, invert_logic):
"""Initialize the RPi binary sensor."""
self._port = port
self._name = name or DEVICE_DEFAULT_NAME
self._invert_logic = invert_logic
self._state = None
def read_pfio(port):
"""Read state from PFIO."""
self._state = rpi_pfio.read_input(self._port)
self.schedule_update_ha_state()
rpi_pfio.edge_detect(hass, self._port, read_pfio, settle_time)
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def is_on(self):
"""Return the state of the entity."""
return self._state != self._invert_logic
def update(self):
"""Update the PFIO state."""
self._state = rpi_pfio.read_input(self._port)

View File

@@ -4,6 +4,7 @@ Support for Wink binary sensors.
For more details about this platform, please refer to the documentation at
at https://home-assistant.io/components/binary_sensor.wink/
"""
import asyncio
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
@@ -101,6 +102,11 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
else:
self.capability = None
@asyncio.coroutine
def async_added_to_hass(self):
"""Callback when entity is added to hass."""
self.hass.data[DOMAIN]['entities']['binary_sensor'].append(self)
@property
def is_on(self):
"""Return true if the binary sensor is on."""

View File

@@ -13,7 +13,7 @@ from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, ATTR_FRIENDLY_NAME, ATTR_ARMED)
from homeassistant.helpers import discovery
REQUIREMENTS = ['blinkpy==0.5.2']
REQUIREMENTS = ['blinkpy==0.6.0']
_LOGGER = logging.getLogger(__name__)

View File

@@ -1,82 +1,82 @@
"""
Demo platform that has two fake binary sensors.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
import homeassistant.util.dt as dt_util
from homeassistant.components.calendar import CalendarEventDevice
from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Demo Calendar platform."""
calendar_data_future = DemoGoogleCalendarDataFuture()
calendar_data_current = DemoGoogleCalendarDataCurrent()
add_devices([
DemoGoogleCalendar(hass, calendar_data_future, {
CONF_NAME: 'Future Event',
CONF_DEVICE_ID: 'future_event',
}),
DemoGoogleCalendar(hass, calendar_data_current, {
CONF_NAME: 'Current Event',
CONF_DEVICE_ID: 'current_event',
}),
])
class DemoGoogleCalendarData(object):
"""Representation of a Demo Calendar element."""
# pylint: disable=no-self-use
def update(self):
"""Return true so entity knows we have new data."""
return True
class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
"""Representation of a Demo Calendar for a future event."""
def __init__(self):
"""Set the event to a future event."""
one_hour_from_now = dt_util.now() \
+ dt_util.dt.timedelta(minutes=30)
self.event = {
'start': {
'dateTime': one_hour_from_now.isoformat()
},
'end': {
'dateTime': (one_hour_from_now + dt_util.dt.
timedelta(minutes=60)).isoformat()
},
'summary': 'Future Event',
}
class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData):
"""Representation of a Demo Calendar for a current event."""
def __init__(self):
"""Set the event data."""
middle_of_event = dt_util.now() \
- dt_util.dt.timedelta(minutes=30)
self.event = {
'start': {
'dateTime': middle_of_event.isoformat()
},
'end': {
'dateTime': (middle_of_event + dt_util.dt.
timedelta(minutes=60)).isoformat()
},
'summary': 'Current Event',
}
class DemoGoogleCalendar(CalendarEventDevice):
"""Representation of a Demo Calendar element."""
def __init__(self, hass, calendar_data, data):
"""Initialize Google Calendar but without the API calls."""
self.data = calendar_data
super().__init__(hass, data)
"""
Demo platform that has two fake binary sensors.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
import homeassistant.util.dt as dt_util
from homeassistant.components.calendar import CalendarEventDevice
from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Demo Calendar platform."""
calendar_data_future = DemoGoogleCalendarDataFuture()
calendar_data_current = DemoGoogleCalendarDataCurrent()
add_devices([
DemoGoogleCalendar(hass, calendar_data_future, {
CONF_NAME: 'Future Event',
CONF_DEVICE_ID: 'future_event',
}),
DemoGoogleCalendar(hass, calendar_data_current, {
CONF_NAME: 'Current Event',
CONF_DEVICE_ID: 'current_event',
}),
])
class DemoGoogleCalendarData(object):
"""Representation of a Demo Calendar element."""
# pylint: disable=no-self-use
def update(self):
"""Return true so entity knows we have new data."""
return True
class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
"""Representation of a Demo Calendar for a future event."""
def __init__(self):
"""Set the event to a future event."""
one_hour_from_now = dt_util.now() \
+ dt_util.dt.timedelta(minutes=30)
self.event = {
'start': {
'dateTime': one_hour_from_now.isoformat()
},
'end': {
'dateTime': (one_hour_from_now + dt_util.dt.
timedelta(minutes=60)).isoformat()
},
'summary': 'Future Event',
}
class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData):
"""Representation of a Demo Calendar for a current event."""
def __init__(self):
"""Set the event data."""
middle_of_event = dt_util.now() \
- dt_util.dt.timedelta(minutes=30)
self.event = {
'start': {
'dateTime': middle_of_event.isoformat()
},
'end': {
'dateTime': (middle_of_event + dt_util.dt.
timedelta(minutes=60)).isoformat()
},
'summary': 'Current Event',
}
class DemoGoogleCalendar(CalendarEventDevice):
"""Representation of a Demo Calendar element."""
def __init__(self, hass, calendar_data, data):
"""Initialize Google Calendar but without the API calls."""
self.data = calendar_data
super().__init__(hass, data)

View File

@@ -1,78 +1,78 @@
"""
Support for Google Calendar Search binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.google_calendar/
"""
# pylint: disable=import-error
import logging
from datetime import timedelta
from homeassistant.components.calendar import CalendarEventDevice
from homeassistant.components.google import (
CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE,
GoogleCalendarService)
from homeassistant.util import Throttle, dt
_LOGGER = logging.getLogger(__name__)
DEFAULT_GOOGLE_SEARCH_PARAMS = {
'orderBy': 'startTime',
'maxResults': 1,
'singleEvents': True,
}
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
def setup_platform(hass, config, add_devices, disc_info=None):
"""Set up the calendar platform for event devices."""
if disc_info is None:
return
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
return
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
add_devices([GoogleCalendarEventDevice(hass, calendar_service,
disc_info[CONF_CAL_ID], data)
for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]])
# pylint: disable=too-many-instance-attributes
class GoogleCalendarEventDevice(CalendarEventDevice):
"""A calendar event device."""
def __init__(self, hass, calendar_service, calendar, data):
"""Create the Calendar event device."""
self.data = GoogleCalendarData(calendar_service, calendar,
data.get('search', None))
super().__init__(hass, data)
class GoogleCalendarData(object):
"""Class to utilize calendar service object to get next event."""
def __init__(self, calendar_service, calendar_id, search=None):
"""Set up how we are going to search the google calendar."""
self.calendar_service = calendar_service
self.calendar_id = calendar_id
self.search = search
self.event = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data."""
service = self.calendar_service.get()
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
params['timeMin'] = dt.now().isoformat('T')
params['calendarId'] = self.calendar_id
if self.search:
params['q'] = self.search
events = service.events() # pylint: disable=no-member
result = events.list(**params).execute()
items = result.get('items', [])
self.event = items[0] if len(items) == 1 else None
return True
"""
Support for Google Calendar Search binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.google_calendar/
"""
# pylint: disable=import-error
import logging
from datetime import timedelta
from homeassistant.components.calendar import CalendarEventDevice
from homeassistant.components.google import (
CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE,
GoogleCalendarService)
from homeassistant.util import Throttle, dt
_LOGGER = logging.getLogger(__name__)
DEFAULT_GOOGLE_SEARCH_PARAMS = {
'orderBy': 'startTime',
'maxResults': 1,
'singleEvents': True,
}
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
def setup_platform(hass, config, add_devices, disc_info=None):
"""Set up the calendar platform for event devices."""
if disc_info is None:
return
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
return
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
add_devices([GoogleCalendarEventDevice(hass, calendar_service,
disc_info[CONF_CAL_ID], data)
for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]])
# pylint: disable=too-many-instance-attributes
class GoogleCalendarEventDevice(CalendarEventDevice):
"""A calendar event device."""
def __init__(self, hass, calendar_service, calendar, data):
"""Create the Calendar event device."""
self.data = GoogleCalendarData(calendar_service, calendar,
data.get('search', None))
super().__init__(hass, data)
class GoogleCalendarData(object):
"""Class to utilize calendar service object to get next event."""
def __init__(self, calendar_service, calendar_id, search=None):
"""Set up how we are going to search the google calendar."""
self.calendar_service = calendar_service
self.calendar_id = calendar_id
self.search = search
self.event = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data."""
service = self.calendar_service.get()
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
params['timeMin'] = dt.now().isoformat('T')
params['calendarId'] = self.calendar_id
if self.search:
params['q'] = self.search
events = service.events() # pylint: disable=no-member
result = events.list(**params).execute()
items = result.get('items', [])
self.event = items[0] if len(items) == 1 else None
return True

View File

@@ -138,7 +138,7 @@ class Camera(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(None, self.camera_image)
return self.hass.async_add_job(self.camera_image)
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
@@ -241,7 +241,7 @@ class CameraView(HomeAssistantView):
return web.Response(status=status)
authenticated = (request[KEY_AUTHENTICATED] or
request.GET.get('token') in camera.access_tokens)
request.query.get('token') in camera.access_tokens)
if not authenticated:
return web.Response(status=401)
@@ -269,7 +269,7 @@ class CameraImageView(CameraView):
image = yield from camera.async_camera_image()
if image:
return web.Response(body=image)
return web.Response(body=image, content_type='image/jpeg')
return web.Response(status=500)

View File

@@ -12,18 +12,22 @@ import voluptuous as vol
import homeassistant.loader as loader
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_aiohttp_proxy_web)
async_get_clientsession, async_aiohttp_proxy_web,
async_aiohttp_proxy_stream)
REQUIREMENTS = ['amcrest==1.1.9']
REQUIREMENTS = ['amcrest==1.2.0']
DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__)
CONF_RESOLUTION = 'resolution'
CONF_STREAM_SOURCE = 'stream_source'
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
DEFAULT_NAME = 'Amcrest Camera'
DEFAULT_PORT = 80
@@ -40,7 +44,8 @@ RESOLUTION_LIST = {
STREAM_SOURCE_LIST = {
'mjpeg': 0,
'snapshot': 1
'snapshot': 1,
'rtsp': 2,
}
CONTENT_TYPE_HEADER = 'Content-Type'
@@ -56,6 +61,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
vol.All(vol.In(STREAM_SOURCE_LIST)),
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
})
@@ -92,8 +98,9 @@ class AmcrestCam(Camera):
super(AmcrestCam, self).__init__()
self._camera = camera
self._base_url = self._camera.get_base_url()
self._hass = hass
self._name = device_info.get(CONF_NAME)
self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)]
self._stream_source = STREAM_SOURCE_LIST[
device_info.get(CONF_STREAM_SOURCE)
@@ -117,15 +124,28 @@ class AmcrestCam(Camera):
yield from super().handle_async_mjpeg_stream(request)
return
# Otherwise, stream an MJPEG image stream directly from the camera
websession = async_get_clientsession(self.hass)
streaming_url = '{0}mjpg/video.cgi?channel=0&subtype={1}'.format(
self._base_url, self._resolution)
elif self._stream_source == STREAM_SOURCE_LIST['mjpeg']:
# stream an MJPEG image stream directly from the camera
websession = async_get_clientsession(self.hass)
streaming_url = self._camera.mjpeg_url(typeno=self._resolution)
stream_coro = websession.get(
streaming_url, auth=self._token, timeout=TIMEOUT)
stream_coro = websession.get(
streaming_url, auth=self._token, timeout=TIMEOUT)
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
else:
# streaming via fmpeg
from haffmpeg import CameraMjpeg
streaming_url = self._camera.rtsp_url(typeno=self._resolution)
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
yield from stream.open_camera(
streaming_url, extra_cmd=self._ffmpeg_arguments)
yield from async_aiohttp_proxy_stream(
self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver')
yield from stream.close()
@property
def name(self):

View File

@@ -0,0 +1,92 @@
"""
This component provides basic support for Netgear Arlo IP cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.arlo/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from homeassistant.components.arlo import DEFAULT_BRAND
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.helpers.aiohttp_client import (
async_aiohttp_proxy_stream)
DEPENDENCIES = ['arlo', 'ffmpeg']
_LOGGER = logging.getLogger(__name__)
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_FFMPEG_ARGUMENTS):
cv.string,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up an Arlo IP Camera."""
arlo = hass.data.get('arlo')
if not arlo:
return False
cameras = []
for camera in arlo.cameras:
cameras.append(ArloCam(hass, camera, config))
async_add_devices(cameras, True)
return True
class ArloCam(Camera):
"""An implementation of a Netgear Arlo IP camera."""
def __init__(self, hass, camera, device_info):
"""Initialize an Arlo camera."""
super().__init__()
self._camera = camera
self._name = self._camera.name
self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
def camera_image(self):
"""Return a still image reponse from the camera."""
return self._camera.last_image
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpeg
video = self._camera.last_video
if not video:
return
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
yield from stream.open_camera(
video.video_url, extra_cmd=self._ffmpeg_arguments)
yield from async_aiohttp_proxy_stream(
self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver')
yield from stream.close()
@property
def name(self):
"""Return the name of this camera."""
return self._name
@property
def model(self):
"""Camera model."""
return self._camera.model_id
@property
def brand(self):
"""Camera brand."""
return DEFAULT_BRAND

View File

@@ -0,0 +1,38 @@
"""
Support for Axis camera streaming.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.axis/
"""
import logging
from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.components.camera.mjpeg import (
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['axis']
DOMAIN = 'axis'
def _get_image_url(host, mode):
if mode == 'mjpeg':
return 'http://{}/axis-cgi/mjpg/video.cgi'.format(host)
elif mode == 'single':
return 'http://{}/axis-cgi/jpg/image.cgi'.format(host)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Axis camera."""
device_info = {
CONF_NAME: discovery_info['name'],
CONF_USERNAME: discovery_info['username'],
CONF_PASSWORD: discovery_info['password'],
CONF_MJPEG_URL: _get_image_url(discovery_info['host'], 'mjpeg'),
CONF_STILL_IMAGE_URL: _get_image_url(discovery_info['host'], 'single'),
CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION,
}
add_devices([MjpegCamera(hass, device_info)])

View File

@@ -103,8 +103,8 @@ class GenericCamera(Camera):
_LOGGER.error("Error getting camera image: %s", error)
return self._last_image
self._last_image = yield from self.hass.loop.run_in_executor(
None, fetch)
self._last_image = yield from self.hass.async_add_job(
fetch)
# async
else:
try:

View File

@@ -88,8 +88,8 @@ class MjpegCamera(Camera):
# DigestAuth is not supported
if self._authentication == HTTP_DIGEST_AUTHENTICATION or \
self._still_image_url is None:
image = yield from self.hass.loop.run_in_executor(
None, self.camera_image)
image = yield from self.hass.async_add_job(
self.camera_image)
return image
websession = async_get_clientsession(self.hass)

View File

@@ -1,250 +1,250 @@
"""
Support for Synology Surveillance Station Cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.synology/
"""
import asyncio
import logging
import voluptuous as vol
import aiohttp
import async_timeout
from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, CONF_TIMEOUT)
from homeassistant.components.camera import (
Camera, PLATFORM_SCHEMA)
from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_create_clientsession,
async_aiohttp_proxy_web)
import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Synology Camera'
DEFAULT_STREAM_ID = '0'
DEFAULT_TIMEOUT = 5
CONF_CAMERA_NAME = 'camera_name'
CONF_STREAM_ID = 'stream_id'
QUERY_CGI = 'query.cgi'
QUERY_API = 'SYNO.API.Info'
AUTH_API = 'SYNO.API.Auth'
CAMERA_API = 'SYNO.SurveillanceStation.Camera'
STREAMING_API = 'SYNO.SurveillanceStation.VideoStream'
SESSION_ID = '0'
WEBAPI_PATH = '/webapi/'
AUTH_PATH = 'auth.cgi'
CAMERA_PATH = 'camera.cgi'
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
CONTENT_TYPE_HEADER = 'Content-Type'
SYNO_API_URL = '{0}{1}{2}'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_URL): cv.string,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up a Synology IP Camera."""
verify_ssl = config.get(CONF_VERIFY_SSL)
timeout = config.get(CONF_TIMEOUT)
websession_init = async_get_clientsession(hass, verify_ssl)
# Determine API to use for authentication
syno_api_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
query_payload = {
'api': QUERY_API,
'method': 'Query',
'version': '1',
'query': 'SYNO.'
}
try:
with async_timeout.timeout(timeout, loop=hass.loop):
query_req = yield from websession_init.get(
syno_api_url,
params=query_payload
)
# Skip content type check because Synology doesn't return JSON with
# right content type
query_resp = yield from query_req.json(content_type=None)
auth_path = query_resp['data'][AUTH_API]['path']
camera_api = query_resp['data'][CAMERA_API]['path']
camera_path = query_resp['data'][CAMERA_API]['path']
streaming_path = query_resp['data'][STREAMING_API]['path']
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.exception("Error on %s", syno_api_url)
return False
# Authticate to NAS to get a session id
syno_auth_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, auth_path)
session_id = yield from get_session_id(
hass,
websession_init,
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
syno_auth_url,
timeout
)
# init websession
websession = async_create_clientsession(
hass, verify_ssl, cookies={'id': session_id})
# Use SessionID to get cameras in system
syno_camera_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, camera_api)
camera_payload = {
'api': CAMERA_API,
'method': 'List',
'version': '1'
}
try:
with async_timeout.timeout(timeout, loop=hass.loop):
camera_req = yield from websession.get(
syno_camera_url,
params=camera_payload
)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.exception("Error on %s", syno_camera_url)
return False
camera_resp = yield from camera_req.json(content_type=None)
cameras = camera_resp['data']['cameras']
# add cameras
devices = []
for camera in cameras:
if not config.get(CONF_WHITELIST):
camera_id = camera['id']
snapshot_path = camera['snapshot_path']
device = SynologyCamera(
hass, websession, config, camera_id, camera['name'],
snapshot_path, streaming_path, camera_path, auth_path, timeout
)
devices.append(device)
async_add_devices(devices)
@asyncio.coroutine
def get_session_id(hass, websession, username, password, login_url, timeout):
"""Get a session id."""
auth_payload = {
'api': AUTH_API,
'method': 'Login',
'version': '2',
'account': username,
'passwd': password,
'session': 'SurveillanceStation',
'format': 'sid'
}
try:
with async_timeout.timeout(timeout, loop=hass.loop):
auth_req = yield from websession.get(
login_url,
params=auth_payload
)
auth_resp = yield from auth_req.json(content_type=None)
return auth_resp['data']['sid']
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.exception("Error on %s", login_url)
return False
class SynologyCamera(Camera):
"""An implementation of a Synology NAS based IP camera."""
def __init__(self, hass, websession, config, camera_id,
camera_name, snapshot_path, streaming_path, camera_path,
auth_path, timeout):
"""Initialize a Synology Surveillance Station camera."""
super().__init__()
self.hass = hass
self._websession = websession
self._name = camera_name
self._synology_url = config.get(CONF_URL)
self._camera_name = config.get(CONF_CAMERA_NAME)
self._stream_id = config.get(CONF_STREAM_ID)
self._camera_id = camera_id
self._snapshot_path = snapshot_path
self._streaming_path = streaming_path
self._camera_path = camera_path
self._auth_path = auth_path
self._timeout = timeout
def camera_image(self):
"""Return bytes of camera image."""
return run_coroutine_threadsafe(
self.async_camera_image(), self.hass.loop).result()
@asyncio.coroutine
def async_camera_image(self):
"""Return a still image response from the camera."""
image_url = SYNO_API_URL.format(
self._synology_url, WEBAPI_PATH, self._camera_path)
image_payload = {
'api': CAMERA_API,
'method': 'GetSnapshot',
'version': '1',
'cameraId': self._camera_id
}
try:
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
response = yield from self._websession.get(
image_url,
params=image_payload
)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Error fetching %s", image_url)
return None
image = yield from response.read()
return image
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
"""Return a MJPEG stream image response directly from the camera."""
streaming_url = SYNO_API_URL.format(
self._synology_url, WEBAPI_PATH, self._streaming_path)
streaming_payload = {
'api': STREAMING_API,
'method': 'Stream',
'version': '1',
'cameraId': self._camera_id,
'format': 'mjpeg'
}
stream_coro = self._websession.get(
streaming_url, params=streaming_payload)
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
@property
def name(self):
"""Return the name of this device."""
return self._name
"""
Support for Synology Surveillance Station Cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.synology/
"""
import asyncio
import logging
import voluptuous as vol
import aiohttp
import async_timeout
from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, CONF_TIMEOUT)
from homeassistant.components.camera import (
Camera, PLATFORM_SCHEMA)
from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_create_clientsession,
async_aiohttp_proxy_web)
import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Synology Camera'
DEFAULT_STREAM_ID = '0'
DEFAULT_TIMEOUT = 5
CONF_CAMERA_NAME = 'camera_name'
CONF_STREAM_ID = 'stream_id'
QUERY_CGI = 'query.cgi'
QUERY_API = 'SYNO.API.Info'
AUTH_API = 'SYNO.API.Auth'
CAMERA_API = 'SYNO.SurveillanceStation.Camera'
STREAMING_API = 'SYNO.SurveillanceStation.VideoStream'
SESSION_ID = '0'
WEBAPI_PATH = '/webapi/'
AUTH_PATH = 'auth.cgi'
CAMERA_PATH = 'camera.cgi'
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
CONTENT_TYPE_HEADER = 'Content-Type'
SYNO_API_URL = '{0}{1}{2}'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_URL): cv.string,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up a Synology IP Camera."""
verify_ssl = config.get(CONF_VERIFY_SSL)
timeout = config.get(CONF_TIMEOUT)
websession_init = async_get_clientsession(hass, verify_ssl)
# Determine API to use for authentication
syno_api_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
query_payload = {
'api': QUERY_API,
'method': 'Query',
'version': '1',
'query': 'SYNO.'
}
try:
with async_timeout.timeout(timeout, loop=hass.loop):
query_req = yield from websession_init.get(
syno_api_url,
params=query_payload
)
# Skip content type check because Synology doesn't return JSON with
# right content type
query_resp = yield from query_req.json(content_type=None)
auth_path = query_resp['data'][AUTH_API]['path']
camera_api = query_resp['data'][CAMERA_API]['path']
camera_path = query_resp['data'][CAMERA_API]['path']
streaming_path = query_resp['data'][STREAMING_API]['path']
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.exception("Error on %s", syno_api_url)
return False
# Authticate to NAS to get a session id
syno_auth_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, auth_path)
session_id = yield from get_session_id(
hass,
websession_init,
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
syno_auth_url,
timeout
)
# init websession
websession = async_create_clientsession(
hass, verify_ssl, cookies={'id': session_id})
# Use SessionID to get cameras in system
syno_camera_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, camera_api)
camera_payload = {
'api': CAMERA_API,
'method': 'List',
'version': '1'
}
try:
with async_timeout.timeout(timeout, loop=hass.loop):
camera_req = yield from websession.get(
syno_camera_url,
params=camera_payload
)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.exception("Error on %s", syno_camera_url)
return False
camera_resp = yield from camera_req.json(content_type=None)
cameras = camera_resp['data']['cameras']
# add cameras
devices = []
for camera in cameras:
if not config.get(CONF_WHITELIST):
camera_id = camera['id']
snapshot_path = camera['snapshot_path']
device = SynologyCamera(
hass, websession, config, camera_id, camera['name'],
snapshot_path, streaming_path, camera_path, auth_path, timeout
)
devices.append(device)
async_add_devices(devices)
@asyncio.coroutine
def get_session_id(hass, websession, username, password, login_url, timeout):
"""Get a session id."""
auth_payload = {
'api': AUTH_API,
'method': 'Login',
'version': '2',
'account': username,
'passwd': password,
'session': 'SurveillanceStation',
'format': 'sid'
}
try:
with async_timeout.timeout(timeout, loop=hass.loop):
auth_req = yield from websession.get(
login_url,
params=auth_payload
)
auth_resp = yield from auth_req.json(content_type=None)
return auth_resp['data']['sid']
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.exception("Error on %s", login_url)
return False
class SynologyCamera(Camera):
"""An implementation of a Synology NAS based IP camera."""
def __init__(self, hass, websession, config, camera_id,
camera_name, snapshot_path, streaming_path, camera_path,
auth_path, timeout):
"""Initialize a Synology Surveillance Station camera."""
super().__init__()
self.hass = hass
self._websession = websession
self._name = camera_name
self._synology_url = config.get(CONF_URL)
self._camera_name = config.get(CONF_CAMERA_NAME)
self._stream_id = config.get(CONF_STREAM_ID)
self._camera_id = camera_id
self._snapshot_path = snapshot_path
self._streaming_path = streaming_path
self._camera_path = camera_path
self._auth_path = auth_path
self._timeout = timeout
def camera_image(self):
"""Return bytes of camera image."""
return run_coroutine_threadsafe(
self.async_camera_image(), self.hass.loop).result()
@asyncio.coroutine
def async_camera_image(self):
"""Return a still image response from the camera."""
image_url = SYNO_API_URL.format(
self._synology_url, WEBAPI_PATH, self._camera_path)
image_payload = {
'api': CAMERA_API,
'method': 'GetSnapshot',
'version': '1',
'cameraId': self._camera_id
}
try:
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
response = yield from self._websession.get(
image_url,
params=image_payload
)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Error fetching %s", image_url)
return None
image = yield from response.read()
return image
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
"""Return a MJPEG stream image response directly from the camera."""
streaming_url = SYNO_API_URL.format(
self._synology_url, WEBAPI_PATH, self._streaming_path)
streaming_payload = {
'api': STREAMING_API,
'method': 'Stream',
'version': '1',
'cameraId': self._camera_id,
'format': 'mjpeg'
}
stream_coro = self._websession.get(
streaming_url, params=streaming_payload)
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
@property
def name(self):
"""Return the name of this device."""
return self._name

View File

@@ -20,12 +20,15 @@ _LOGGER = logging.getLogger(__name__)
CONF_NVR = 'nvr'
CONF_KEY = 'key'
CONF_PASSWORD = 'password'
DEFAULT_PASSWORD = 'ubnt'
DEFAULT_PORT = 7080
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NVR): cv.string,
vol.Required(CONF_KEY): cv.string,
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
@@ -34,6 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Discover cameras on a Unifi NVR."""
addr = config[CONF_NVR]
key = config[CONF_KEY]
password = config[CONF_PASSWORD]
port = config[CONF_PORT]
from uvcclient import nvr
@@ -59,7 +63,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([UnifiVideoCamera(nvrconn,
camera[identifier],
camera['name'])
camera['name'],
password)
for camera in cameras])
return True
@@ -67,12 +72,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class UnifiVideoCamera(Camera):
"""A Ubiquiti Unifi Video Camera."""
def __init__(self, nvr, uuid, name):
def __init__(self, nvr, uuid, name, password):
"""Initialize an Unifi camera."""
super(UnifiVideoCamera, self).__init__()
self._nvr = nvr
self._uuid = uuid
self._name = name
self._password = password
self.is_streaming = False
self._connect_addr = None
self._camera = None
@@ -102,7 +108,6 @@ class UnifiVideoCamera(Camera):
def _login(self):
"""Login to the camera."""
from uvcclient import camera as uvc_camera
from uvcclient import store as uvc_store
caminfo = self._nvr.get_camera(self._uuid)
if self._connect_addr:
@@ -110,13 +115,6 @@ class UnifiVideoCamera(Camera):
else:
addrs = [caminfo['host'], caminfo['internalHost']]
store = uvc_store.get_info_store()
password = store.get_camera_password(self._uuid)
if password is None:
_LOGGER.debug("Logging into camera %(name)s with default password",
dict(name=self._name))
password = 'ubnt'
if self._nvr.server_version >= (3, 2, 0):
client_cls = uvc_camera.UVCCameraClientV320
else:
@@ -126,7 +124,7 @@ class UnifiVideoCamera(Camera):
for addr in addrs:
try:
camera = client_cls(
addr, caminfo['username'], password)
addr, caminfo['username'], self._password)
camera.login()
_LOGGER.debug("Logged into UVC camera %(name)s via %(addr)s",
dict(name=self._name, addr=addr))

View File

@@ -107,12 +107,7 @@ class ZoneMinderCamera(MjpegCamera):
self._monitor_id)
return
if not status_response.get("success", False):
_LOGGER.warning("Alarm status API call failed for monitor %i",
self._monitor_id)
return
self._is_recording = status_response['status'] == ZM_STATE_ALARM
self._is_recording = status_response.get('status') == ZM_STATE_ALARM
@property
def is_recording(self):

View File

@@ -213,8 +213,8 @@ def async_setup(hass, config):
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
yield from component.async_setup(config)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file,
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine
@@ -569,8 +569,8 @@ class ClimateDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.set_temperature, **kwargs))
return self.hass.async_add_job(
ft.partial(self.set_temperature, **kwargs))
def set_humidity(self, humidity):
"""Set new target humidity."""
@@ -581,8 +581,7 @@ class ClimateDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.set_humidity, humidity)
return self.hass.async_add_job(self.set_humidity, humidity)
def set_fan_mode(self, fan):
"""Set new target fan mode."""
@@ -593,8 +592,7 @@ class ClimateDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.set_fan_mode, fan)
return self.hass.async_add_job(self.set_fan_mode, fan)
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
@@ -605,8 +603,7 @@ class ClimateDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.set_operation_mode, operation_mode)
return self.hass.async_add_job(self.set_operation_mode, operation_mode)
def set_swing_mode(self, swing_mode):
"""Set new target swing operation."""
@@ -617,8 +614,7 @@ class ClimateDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.set_swing_mode, swing_mode)
return self.hass.async_add_job(self.set_swing_mode, swing_mode)
def turn_away_mode_on(self):
"""Turn away mode on."""
@@ -629,8 +625,7 @@ class ClimateDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.turn_away_mode_on)
return self.hass.async_add_job(self.turn_away_mode_on)
def turn_away_mode_off(self):
"""Turn away mode off."""
@@ -641,8 +636,7 @@ class ClimateDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.turn_away_mode_off)
return self.hass.async_add_job(self.turn_away_mode_off)
def set_hold_mode(self, hold_mode):
"""Set new target hold mode."""
@@ -653,8 +647,7 @@ class ClimateDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.set_hold_mode, hold_mode)
return self.hass.async_add_job(self.set_hold_mode, hold_mode)
def turn_aux_heat_on(self):
"""Turn auxillary heater on."""
@@ -665,8 +658,7 @@ class ClimateDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.turn_aux_heat_on)
return self.hass.async_add_job(self.turn_aux_heat_on)
def turn_aux_heat_off(self):
"""Turn auxillary heater off."""
@@ -677,8 +669,7 @@ class ClimateDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.turn_aux_heat_off)
return self.hass.async_add_job(self.turn_aux_heat_off)
@property
def min_temp(self):

View File

@@ -149,22 +149,22 @@ class SensiboClimate(ClimateDevice):
@property
def current_fan_mode(self):
"""Return the fan setting."""
return self._ac_states['fanLevel']
return self._ac_states.get('fanLevel')
@property
def fan_list(self):
"""List of available fan modes."""
return self._current_capabilities['fanLevels']
return self._current_capabilities.get('fanLevels')
@property
def current_swing_mode(self):
"""Return the fan setting."""
return self._ac_states['swing']
return self._ac_states.get('swing')
@property
def swing_list(self):
"""List of available swing modes."""
return self._current_capabilities['swing']
return self._current_capabilities.get('swing')
@property
def name(self):

View File

@@ -1,4 +1,4 @@
"""
"""
Tado component to create a climate device for each zone.
For more details about this platform, please refer to the documentation at

View File

@@ -4,6 +4,8 @@ Support for Wink thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.wink/
"""
import asyncio
from homeassistant.components.wink import WinkDevice, DOMAIN
from homeassistant.components.climate import (
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
@@ -52,6 +54,11 @@ class WinkThermostat(WinkDevice, ClimateDevice):
super().__init__(wink, hass)
self._config_temp_unit = temp_unit
@asyncio.coroutine
def async_added_to_hass(self):
"""Callback when entity is added to hass."""
self.hass.data[DOMAIN]['entities']['climate'].append(self)
@property
def temperature_unit(self):
"""Return the unit of measurement."""

View File

@@ -5,7 +5,7 @@ import os
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import EVENT_COMPONENT_LOADED
from homeassistant.const import EVENT_COMPONENT_LOADED, CONF_ID
from homeassistant.setup import (
async_prepare_setup_platform, ATTR_COMPONENT)
from homeassistant.components.frontend import register_built_in_panel
@@ -14,8 +14,8 @@ from homeassistant.util.yaml import load_yaml, dump
DOMAIN = 'config'
DEPENDENCIES = ['http']
SECTIONS = ('core', 'group', 'hassbian')
ON_DEMAND = ('zwave', )
SECTIONS = ('core', 'group', 'hassbian', 'automation')
ON_DEMAND = ('zwave')
@asyncio.coroutine
@@ -60,7 +60,7 @@ def async_setup(hass, config):
return True
class EditKeyBasedConfigView(HomeAssistantView):
class BaseEditConfigView(HomeAssistantView):
"""Configure a Group endpoint."""
def __init__(self, component, config_type, path, key_schema, data_schema,
@@ -73,13 +73,29 @@ class EditKeyBasedConfigView(HomeAssistantView):
self.data_schema = data_schema
self.post_write_hook = post_write_hook
def _empty_config(self):
"""Empty config if file not found."""
raise NotImplementedError
def _get_value(self, data, config_key):
"""Get value."""
raise NotImplementedError
def _write_value(self, data, config_key, new_value):
"""Set value."""
raise NotImplementedError
@asyncio.coroutine
def get(self, request, config_key):
"""Fetch device specific config."""
hass = request.app['hass']
current = yield from hass.loop.run_in_executor(
None, _read, hass.config.path(self.path))
return self.json(current.get(config_key, {}))
current = yield from self.read_config(hass)
value = self._get_value(current, config_key)
if value is None:
return self.json_message('Resource not found', 404)
return self.json(value)
@asyncio.coroutine
def post(self, request, config_key):
@@ -104,10 +120,10 @@ class EditKeyBasedConfigView(HomeAssistantView):
hass = request.app['hass']
path = hass.config.path(self.path)
current = yield from hass.loop.run_in_executor(None, _read, path)
current.setdefault(config_key, {}).update(data)
current = yield from self.read_config(hass)
self._write_value(current, config_key, data)
yield from hass.loop.run_in_executor(None, _write, path, current)
yield from hass.async_add_job(_write, path, current)
if self.post_write_hook is not None:
hass.async_add_job(self.post_write_hook(hass))
@@ -116,13 +132,59 @@ class EditKeyBasedConfigView(HomeAssistantView):
'result': 'ok',
})
@asyncio.coroutine
def read_config(self, hass):
"""Read the config."""
current = yield from hass.async_add_job(
_read, hass.config.path(self.path))
if not current:
current = self._empty_config()
return current
class EditKeyBasedConfigView(BaseEditConfigView):
"""Configure a list of entries."""
def _empty_config(self):
"""Return an empty config."""
return {}
def _get_value(self, data, config_key):
"""Get value."""
return data.get(config_key, {})
def _write_value(self, data, config_key, new_value):
"""Set value."""
data.setdefault(config_key, {}).update(new_value)
class EditIdBasedConfigView(BaseEditConfigView):
"""Configure key based config entries."""
def _empty_config(self):
"""Return an empty config."""
return []
def _get_value(self, data, config_key):
"""Get value."""
return next(
(val for val in data if val.get(CONF_ID) == config_key), None)
def _write_value(self, data, config_key, new_value):
"""Set value."""
value = self._get_value(data, config_key)
if value is None:
value = {CONF_ID: config_key}
data.append(value)
value.update(new_value)
def _read(path):
"""Read YAML helper."""
if not os.path.isfile(path):
with open(path, 'w'):
pass
return {}
return None
return load_yaml(path)

View File

@@ -0,0 +1,20 @@
"""Provide configuration end points for Z-Wave."""
import asyncio
from homeassistant.components.config import EditIdBasedConfigView
from homeassistant.components.automation import (
PLATFORM_SCHEMA, DOMAIN, async_reload)
import homeassistant.helpers.config_validation as cv
CONFIG_PATH = 'automations.yaml'
@asyncio.coroutine
def async_setup(hass):
"""Set up the Automation config API."""
hass.http.register_view(EditIdBasedConfigView(
DOMAIN, 'config', CONFIG_PATH, cv.string,
PLATFORM_SCHEMA, post_write_hook=async_reload
))
return True

View File

@@ -9,9 +9,11 @@ the user has submitted configuration information.
import asyncio
import logging
from homeassistant.core import callback as async_callback
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
ATTR_ENTITY_PICTURE
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.util.async import run_callback_threadsafe
_LOGGER = logging.getLogger(__name__)
_REQUESTS = {}
@@ -43,7 +45,9 @@ def request_config(
Will return an ID to be used for sequent calls.
"""
instance = _get_instance(hass)
instance = run_callback_threadsafe(hass.loop,
_async_get_instance,
hass).result()
request_id = instance.request_config(
name, callback,
@@ -79,7 +83,8 @@ def async_setup(hass, config):
return True
def _get_instance(hass):
@async_callback
def _async_get_instance(hass):
"""Get an instance per hass object."""
instance = hass.data.get(_KEY_INSTANCE)
@@ -97,7 +102,7 @@ class Configurator(object):
self.hass = hass
self._cur_id = 0
self._requests = {}
hass.services.register(
hass.services.async_register(
DOMAIN, SERVICE_CONFIGURE, self.handle_service_call)
def request_config(

View File

@@ -175,8 +175,8 @@ def async_setup(hass, config):
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
for service_name in SERVICE_TO_METHOD:
@@ -263,8 +263,7 @@ class CoverDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.open_cover, **kwargs))
return self.hass.async_add_job(ft.partial(self.open_cover, **kwargs))
def close_cover(self, **kwargs):
"""Close cover."""
@@ -275,8 +274,7 @@ class CoverDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.close_cover, **kwargs))
return self.hass.async_add_job(ft.partial(self.close_cover, **kwargs))
def set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
@@ -287,8 +285,8 @@ class CoverDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.set_cover_position, **kwargs))
return self.hass.async_add_job(
ft.partial(self.set_cover_position, **kwargs))
def stop_cover(self, **kwargs):
"""Stop the cover."""
@@ -299,8 +297,7 @@ class CoverDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.stop_cover, **kwargs))
return self.hass.async_add_job(ft.partial(self.stop_cover, **kwargs))
def open_cover_tilt(self, **kwargs):
"""Open the cover tilt."""
@@ -311,8 +308,8 @@ class CoverDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.open_cover_tilt, **kwargs))
return self.hass.async_add_job(
ft.partial(self.open_cover_tilt, **kwargs))
def close_cover_tilt(self, **kwargs):
"""Close the cover tilt."""
@@ -323,8 +320,8 @@ class CoverDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.close_cover_tilt, **kwargs))
return self.hass.async_add_job(
ft.partial(self.close_cover_tilt, **kwargs))
def set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
@@ -335,8 +332,8 @@ class CoverDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.set_cover_tilt_position, **kwargs))
return self.hass.async_add_job(
ft.partial(self.set_cover_tilt_position, **kwargs))
def stop_cover_tilt(self, **kwargs):
"""Stop the cover."""
@@ -347,5 +344,5 @@ class CoverDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.stop_cover_tilt, **kwargs))
return self.hass.async_add_job(
ft.partial(self.stop_cover_tilt, **kwargs))

View File

@@ -0,0 +1,62 @@
"""
Support for Lutron Caseta SerenaRollerShade.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.lutron_caseta/
"""
import logging
from homeassistant.components.cover import (
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE)
from homeassistant.components.lutron_caseta import (
LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['lutron_caseta']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Lutron Caseta Serena shades as a cover device."""
devs = []
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
cover_devices = bridge.get_devices_by_types(["SerenaRollerShade"])
for cover_device in cover_devices:
dev = LutronCasetaCover(cover_device, bridge)
devs.append(dev)
add_devices(devs, True)
class LutronCasetaCover(LutronCasetaDevice, CoverDevice):
"""Representation of a Lutron Serena shade."""
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_OPEN | SUPPORT_CLOSE
@property
def is_closed(self):
"""Return if the cover is closed."""
return self._state["current_state"] < 1
def close_cover(self):
"""Close the cover."""
self._smartbridge.set_value(self._device_id, 0)
def open_cover(self):
"""Open the cover."""
self._smartbridge.set_value(self._device_id, 100)
def set_cover_position(self, position, **kwargs):
"""Move the roller shutter to a specific position."""
self._smartbridge.set_value(self._device_id, position)
def update(self):
"""Call when forcing a refresh of the device."""
self._state = self._smartbridge.get_device_by_id(self._device_id)
_LOGGER.debug(self._state)

View File

@@ -40,6 +40,7 @@ CONF_TILT_OPEN_POSITION = 'tilt_opened_value'
CONF_TILT_MIN = 'tilt_min'
CONF_TILT_MAX = 'tilt_max'
CONF_TILT_STATE_OPTIMISTIC = 'tilt_optimistic'
CONF_TILT_INVERT_STATE = 'tilt_invert_state'
DEFAULT_NAME = 'MQTT Cover'
DEFAULT_PAYLOAD_OPEN = 'OPEN'
@@ -52,6 +53,7 @@ DEFAULT_TILT_OPEN_POSITION = 100
DEFAULT_TILT_MIN = 0
DEFAULT_TILT_MAX = 100
DEFAULT_TILT_OPTIMISTIC = False
DEFAULT_TILT_INVERT_STATE = False
TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT |
SUPPORT_SET_TILT_POSITION)
@@ -74,6 +76,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TILT_MAX, default=DEFAULT_TILT_MAX): int,
vol.Optional(CONF_TILT_STATE_OPTIMISTIC,
default=DEFAULT_TILT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_TILT_INVERT_STATE,
default=DEFAULT_TILT_INVERT_STATE): cv.boolean,
})
@@ -104,6 +108,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
config.get(CONF_TILT_MIN),
config.get(CONF_TILT_MAX),
config.get(CONF_TILT_STATE_OPTIMISTIC),
config.get(CONF_TILT_INVERT_STATE),
)])
@@ -114,7 +119,8 @@ class MqttCover(CoverDevice):
tilt_status_topic, qos, retain, state_open, state_closed,
payload_open, payload_close, payload_stop,
optimistic, value_template, tilt_open_position,
tilt_closed_position, tilt_min, tilt_max, tilt_optimistic):
tilt_closed_position, tilt_min, tilt_max, tilt_optimistic,
tilt_invert):
"""Initialize the cover."""
self._position = None
self._state = None
@@ -138,6 +144,7 @@ class MqttCover(CoverDevice):
self._tilt_min = tilt_min
self._tilt_max = tilt_max
self._tilt_optimistic = tilt_optimistic
self._tilt_invert = tilt_invert
@asyncio.coroutine
def async_added_to_hass(self):
@@ -150,8 +157,8 @@ class MqttCover(CoverDevice):
"""Handle tilt updates."""
if (payload.isnumeric() and
self._tilt_min <= int(payload) <= self._tilt_max):
tilt_range = self._tilt_max - self._tilt_min
level = round(float(payload) / tilt_range * 100.0)
level = self.find_percentage_in_range(float(payload))
self._tilt_value = level
self.hass.async_add_job(self.async_update_ha_state())
@@ -278,7 +285,8 @@ class MqttCover(CoverDevice):
def async_open_cover_tilt(self, **kwargs):
"""Tilt the cover open."""
mqtt.async_publish(self.hass, self._tilt_command_topic,
self._tilt_open_position, self._qos, self._retain)
self._tilt_open_position, self._qos,
self._retain)
if self._tilt_optimistic:
self._tilt_value = self._tilt_open_position
self.hass.async_add_job(self.async_update_ha_state())
@@ -287,7 +295,8 @@ class MqttCover(CoverDevice):
def async_close_cover_tilt(self, **kwargs):
"""Tilt the cover closed."""
mqtt.async_publish(self.hass, self._tilt_command_topic,
self._tilt_closed_position, self._qos, self._retain)
self._tilt_closed_position, self._qos,
self._retain)
if self._tilt_optimistic:
self._tilt_value = self._tilt_closed_position
self.hass.async_add_job(self.async_update_ha_state())
@@ -301,9 +310,39 @@ class MqttCover(CoverDevice):
position = float(kwargs[ATTR_TILT_POSITION])
# The position needs to be between min and max
tilt_range = self._tilt_max - self._tilt_min
percentage = position / 100.0
level = round(tilt_range * percentage)
level = self.find_in_range_from_percent(position)
mqtt.async_publish(self.hass, self._tilt_command_topic,
level, self._qos, self._retain)
def find_percentage_in_range(self, position):
"""Find the 0-100% value within the specified range."""
# the range of motion as defined by the min max values
tilt_range = self._tilt_max - self._tilt_min
# offset to be zero based
offset_position = position - self._tilt_min
# the percentage value within the range
position_percentage = float(offset_position) / tilt_range * 100.0
if self._tilt_invert:
return 100 - position_percentage
else:
return position_percentage
def find_in_range_from_percent(self, percentage):
"""
Find the adjusted value for 0-100% within the specified range.
if the range is 80-180 and the percentage is 90
this method would determine the value to send on the topic
by offsetting the max and min, getting the percentage value and
returning the offset
"""
offset = self._tilt_min
tilt_range = self._tilt_max - self._tilt_min
position = round(tilt_range * (percentage / 100.0))
position += offset
if self._tilt_invert:
position = self._tilt_max - position + offset
return position

View File

@@ -12,19 +12,25 @@ from homeassistant.components.cover import CoverDevice
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, STATE_CLOSED)
import homeassistant.helpers.config_validation as cv
import homeassistant.loader as loader
REQUIREMENTS = [
'https://github.com/arraylabs/pymyq/archive/v0.0.8.zip'
'#pymyq==0.0.8']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'myq'
NOTIFICATION_ID = 'myq_notification'
NOTIFICATION_TITLE = 'MyQ Cover Setup'
COVER_SCHEMA = vol.Schema({
vol.Required(CONF_TYPE): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
DEFAULT_NAME = 'myq'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the MyQ component."""
@@ -33,23 +39,28 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
brand = config.get(CONF_TYPE)
logger = logging.getLogger(__name__)
persistent_notification = loader.get_component('persistent_notification')
myq = pymyq(username, password, brand)
if not myq.is_supported_brand():
logger.error("Unsupported type. See documentation")
return
if not myq.is_login_valid():
logger.error("Username or Password is incorrect")
return
try:
if not myq.is_supported_brand():
raise ValueError("Unsupported type. See documentation")
if not myq.is_login_valid():
raise ValueError("Username or Password is incorrect")
add_devices(MyQDevice(myq, door) for door in myq.get_garage_doors())
except (TypeError, KeyError, NameError) as ex:
logger.error("%s", ex)
return True
except (TypeError, KeyError, NameError, ValueError) as ex:
_LOGGER.error("%s", ex)
persistent_notification.create(
hass, 'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
class MyQDevice(CoverDevice):

View File

@@ -4,6 +4,8 @@ Support for Wink Covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.wink/
"""
import asyncio
from homeassistant.components.cover import CoverDevice
from homeassistant.components.wink import WinkDevice, DOMAIN
@@ -31,6 +33,11 @@ class WinkCoverDevice(WinkDevice, CoverDevice):
"""Initialize the cover."""
super().__init__(wink, hass)
@asyncio.coroutine
def async_added_to_hass(self):
"""Callback when entity is added to hass."""
self.hass.data[DOMAIN]['entities']['cover'].append(self)
def close_cover(self):
"""Close the shade."""
self.wink.set_state(0)

View File

@@ -41,7 +41,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
"""Initialize the Z-Wave rollershutter."""
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
# pylint: disable=no-member
self._network = hass.data[zwave.ZWAVE_NETWORK]
self._network = hass.data[zwave.const.DATA_NETWORK]
self._open_id = None
self._close_id = None
self._current_position = None

View File

@@ -0,0 +1,120 @@
"""
A component which allows you to send data to Datadog.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/datadog/
"""
import logging
import voluptuous as vol
from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_PREFIX,
EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED,
STATE_UNKNOWN)
from homeassistant.helpers import state as state_helper
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['datadog==0.15.0']
_LOGGER = logging.getLogger(__name__)
CONF_RATE = 'rate'
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 8125
DEFAULT_PREFIX = 'hass'
DEFAULT_RATE = 1
DOMAIN = 'datadog'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): cv.string,
vol.Optional(CONF_RATE, default=DEFAULT_RATE):
vol.All(vol.Coerce(int), vol.Range(min=1)),
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Setup the Datadog component."""
from datadog import initialize, statsd
conf = config[DOMAIN]
host = conf.get(CONF_HOST)
port = conf.get(CONF_PORT)
sample_rate = conf.get(CONF_RATE)
prefix = conf.get(CONF_PREFIX)
initialize(statsd_host=host, statsd_port=port)
def logbook_entry_listener(event):
"""Listen for logbook entries and send them as events."""
name = event.data.get('name')
message = event.data.get('message')
statsd.event(
title="Home Assistant",
text="%%% \n **{}** {} \n %%%".format(name, message),
tags=[
"entity:{}".format(event.data.get('entity_id')),
"domain:{}".format(event.data.get('domain'))
]
)
_LOGGER.debug('Sent event %s', event.data.get('entity_id'))
def state_changed_listener(event):
"""Listen for new messages on the bus and sends them to Datadog."""
state = event.data.get('new_state')
if state is None or state.state == STATE_UNKNOWN:
return
if state.attributes.get('hidden') is True:
return
states = dict(state.attributes)
metric = "{}.{}".format(prefix, state.domain)
tags = ["entity:{}".format(state.entity_id)]
for key, value in states.items():
if isinstance(value, (float, int)):
attribute = "{}.{}".format(metric, key.replace(' ', '_'))
statsd.gauge(
attribute,
value,
sample_rate=sample_rate,
tags=tags
)
_LOGGER.debug(
'Sent metric %s: %s (tags: %s)',
attribute,
value,
tags
)
try:
value = state_helper.state_as_number(state)
except ValueError:
_LOGGER.debug(
'Error sending %s: %s (tags: %s)',
metric,
state.state,
tags
)
return
statsd.gauge(
metric,
value,
sample_rate=sample_rate,
tags=tags
)
_LOGGER.debug('Sent metric %s: %s (tags: %s)', metric, value, tags)
hass.bus.listen(EVENT_LOGBOOK_ENTRY, logbook_entry_listener)
hass.bus.listen(EVENT_STATE_CHANGED, state_changed_listener)
return True

View File

@@ -157,28 +157,28 @@ def async_setup(hass, config):
}},
]}))
tasks2.append(group.Group.async_create_group(hass, 'living room', [
tasks2.append(group.Group.async_create_group(hass, 'Living Room', [
lights[1], switches[0], 'input_select.living_room_preset',
'cover.living_room_window', media_players[1],
'scene.romantic_lights']))
tasks2.append(group.Group.async_create_group(hass, 'bedroom', [
tasks2.append(group.Group.async_create_group(hass, 'Bedroom', [
lights[0], switches[1], media_players[0],
'input_slider.noise_allowance']))
tasks2.append(group.Group.async_create_group(hass, 'kitchen', [
tasks2.append(group.Group.async_create_group(hass, 'Kitchen', [
lights[2], 'cover.kitchen_window', 'lock.kitchen_door']))
tasks2.append(group.Group.async_create_group(hass, 'doors', [
tasks2.append(group.Group.async_create_group(hass, 'Doors', [
'lock.front_door', 'lock.kitchen_door',
'garage_door.right_garage_door', 'garage_door.left_garage_door']))
tasks2.append(group.Group.async_create_group(hass, 'automations', [
tasks2.append(group.Group.async_create_group(hass, 'Automations', [
'input_select.who_cooks', 'input_boolean.notify', ]))
tasks2.append(group.Group.async_create_group(hass, 'people', [
tasks2.append(group.Group.async_create_group(hass, 'People', [
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
'device_tracker.demo_paulus']))
tasks2.append(group.Group.async_create_group(hass, 'downstairs', [
tasks2.append(group.Group.async_create_group(hass, 'Downstairs', [
'group.living_room', 'group.kitchen',
'scene.romantic_lights', 'cover.kitchen_window',
'cover.living_room_window', 'group.doors',
'thermostat.ecobee',
'climate.ecobee',
], view=True))
results = yield from asyncio.gather(*tasks2, loop=hass.loop)

View File

@@ -14,12 +14,13 @@ from homeassistant.core import callback
import homeassistant.util.dt as dt_util
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
from homeassistant.helpers.event import (
async_track_point_in_time, async_track_state_change)
async_track_point_in_utc_time, async_track_state_change)
from homeassistant.helpers.sun import is_up, get_astral_event_next
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv
DOMAIN = 'device_sun_light_trigger'
DEPENDENCIES = ['light', 'device_tracker', 'group', 'sun']
DEPENDENCIES = ['light', 'device_tracker', 'group']
CONF_DEVICE_GROUP = 'device_group'
CONF_DISABLE_TURN_OFF = 'disable_turn_off'
@@ -50,7 +51,6 @@ def async_setup(hass, config):
device_tracker = get_component('device_tracker')
group = get_component('group')
light = get_component('light')
sun = get_component('sun')
conf = config[DOMAIN]
disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF)
light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS)
@@ -78,7 +78,7 @@ def async_setup(hass, config):
Async friendly.
"""
next_setting = sun.next_setting(hass)
next_setting = get_astral_event_next(hass, 'sunset')
if not next_setting:
return None
return next_setting - LIGHT_TRANSITION_TIME * len(light_ids)
@@ -103,7 +103,7 @@ def async_setup(hass, config):
# Track every time sun rises so we can schedule a time-based
# pre-sun set event
@callback
def schedule_light_turn_on(entity, old_state, new_state):
def schedule_light_turn_on(now):
"""Turn on all the lights at the moment sun sets.
We will schedule to have each light start after one another
@@ -114,26 +114,26 @@ def async_setup(hass, config):
return
for index, light_id in enumerate(light_ids):
async_track_point_in_time(
async_track_point_in_utc_time(
hass, async_turn_on_factory(light_id),
start_point + index * LIGHT_TRANSITION_TIME)
async_track_state_change(hass, sun.ENTITY_ID, schedule_light_turn_on,
sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON)
async_track_point_in_utc_time(hass, schedule_light_turn_on,
get_astral_event_next(hass, 'sunrise'))
# If the sun is already above horizon schedule the time-based pre-sun set
# event.
if sun.is_on(hass):
schedule_light_turn_on(None, None, None)
if is_up(hass):
schedule_light_turn_on(None)
@callback
def check_light_on_dev_state_change(entity, old_state, new_state):
"""Handle tracked device state changes."""
lights_are_on = group.is_on(hass, light_group)
light_needed = not (lights_are_on or sun.is_on(hass))
light_needed = not (lights_are_on or is_up(hass))
# These variables are needed for the elif check
now = dt_util.now()
now = dt_util.utcnow()
start_point = calc_time_for_light_when_sunset()
# Do we need lights?
@@ -146,7 +146,7 @@ def async_setup(hass, config):
# Check this by seeing if current time is later then the point
# in time when we would start putting the lights on.
elif (start_point and
start_point < now < sun.next_setting(hass)):
start_point < now < get_astral_event_next(hass, 'sunset')):
# Check for every light if it would be on if someone was home
# when the fading in started and turn it on if so

View File

@@ -35,7 +35,8 @@ from homeassistant.util.yaml import dump
from homeassistant.helpers.event import async_track_utc_time_change
from homeassistant.const import (
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_MAC,
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID)
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID,
CONF_ICON, ATTR_ICON)
_LOGGER = logging.getLogger(__name__)
@@ -150,14 +151,14 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
scanner = yield from platform.async_get_scanner(
hass, {DOMAIN: p_config})
elif hasattr(platform, 'get_scanner'):
scanner = yield from hass.loop.run_in_executor(
None, platform.get_scanner, hass, {DOMAIN: p_config})
scanner = yield from hass.async_add_job(
platform.get_scanner, hass, {DOMAIN: p_config})
elif hasattr(platform, 'async_setup_scanner'):
setup = yield from platform.async_setup_scanner(
hass, p_config, tracker.async_see, disc_info)
elif hasattr(platform, 'setup_scanner'):
setup = yield from hass.loop.run_in_executor(
None, platform.setup_scanner, hass, p_config, tracker.see,
setup = yield from hass.async_add_job(
platform.setup_scanner, hass, p_config, tracker.see,
disc_info)
else:
raise HomeAssistantError("Invalid device_tracker platform.")
@@ -209,8 +210,8 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)}
yield from tracker.async_see(**args)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file,
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml')
)
hass.services.async_register(
@@ -322,8 +323,8 @@ class DeviceTracker(object):
This method is a coroutine.
"""
with (yield from self._is_updating):
yield from self.hass.loop.run_in_executor(
None, update_config, self.hass.config.path(YAML_DEVICES),
yield from self.hass.async_add_job(
update_config, self.hass.config.path(YAML_DEVICES),
dev_id, device)
@asyncio.coroutine
@@ -381,6 +382,7 @@ class Device(Entity):
battery = None # type: str
attributes = None # type: dict
vendor = None # type: str
icon = None # type: str
# Track if the last update of this device was HOME.
last_update_home = False
@@ -388,7 +390,7 @@ class Device(Entity):
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track: bool, dev_id: str, mac: str, name: str=None,
picture: str=None, gravatar: str=None,
picture: str=None, gravatar: str=None, icon: str=None,
hide_if_away: bool=False, vendor: str=None) -> None:
"""Initialize a device."""
self.hass = hass
@@ -414,6 +416,8 @@ class Device(Entity):
else:
self.config_picture = picture
self.icon = icon
self.away_hide = hide_if_away
self.vendor = vendor
@@ -608,7 +612,7 @@ class DeviceScanner(object):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(None, self.scan_devices)
return self.hass.async_add_job(self.scan_devices)
def get_device_name(self, mac: str) -> str:
"""Get device name from mac."""
@@ -619,7 +623,7 @@ class DeviceScanner(object):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(None, self.get_device_name, mac)
return self.hass.async_add_job(self.get_device_name, mac)
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
@@ -637,6 +641,8 @@ def async_load_config(path: str, hass: HomeAssistantType,
"""
dev_schema = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_ICON, default=False):
vol.Any(None, cv.icon),
vol.Optional('track', default=False): cv.boolean,
vol.Optional(CONF_MAC, default=None):
vol.Any(None, vol.All(cv.string, vol.Upper)),
@@ -650,8 +656,8 @@ def async_load_config(path: str, hass: HomeAssistantType,
try:
result = []
try:
devices = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, path)
devices = yield from hass.async_add_job(
load_yaml_config_file, path)
except HomeAssistantError as err:
_LOGGER.error("Unable to load %s: %s", path, str(err))
return []
@@ -728,6 +734,7 @@ def update_config(path: str, dev_id: str, device: Device):
device = {device.dev_id: {
ATTR_NAME: device.name,
ATTR_MAC: device.mac,
ATTR_ICON: device.icon,
'picture': device.config_picture,
'track': device.track,
CONF_AWAY_HIDE: device.away_hide,

View File

@@ -118,25 +118,29 @@ class AsusWrtDeviceScanner(DeviceScanner):
self.protocol = config[CONF_PROTOCOL]
self.mode = config[CONF_MODE]
self.port = config[CONF_PORT]
self.ssh_args = {}
if self.protocol == 'ssh':
self.ssh_args['port'] = self.port
if self.ssh_key:
self.ssh_args['ssh_key'] = self.ssh_key
elif self.password:
self.ssh_args['password'] = self.password
else:
if not (self.ssh_key or self.password):
_LOGGER.error("No password or private key specified")
self.success_init = False
return
self.connection = SshConnection(self.host, self.port,
self.username,
self.password,
self.ssh_key,
self.mode == "ap")
else:
if not self.password:
_LOGGER.error("No password specified")
self.success_init = False
return
self.connection = TelnetConnection(self.host, self.port,
self.username,
self.password,
self.mode == "ap")
self.lock = threading.Lock()
self.last_results = {}
@@ -182,105 +186,9 @@ class AsusWrtDeviceScanner(DeviceScanner):
self.last_results = active_clients
return True
def ssh_connection(self):
"""Retrieve data from ASUSWRT via the ssh protocol."""
from pexpect import pxssh, exceptions
ssh = pxssh.pxssh()
try:
ssh.login(self.host, self.username, **self.ssh_args)
except exceptions.EOF as err:
_LOGGER.error("Connection refused. SSH enabled?")
return None
except pxssh.ExceptionPxssh as err:
_LOGGER.error("Unable to connect via SSH: %s", str(err))
return None
try:
ssh.sendline(_IP_NEIGH_CMD)
ssh.prompt()
neighbors = 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]
ssh.sendline(_NVRAM_CMD)
ssh.prompt()
nvram_result = ssh.before.split(b'\n')[1].split(b'<')[1:]
else:
arp_result = ['']
nvram_result = ['']
ssh.sendline(_LEASES_CMD)
ssh.prompt()
leases_result = ssh.before.split(b'\n')[1:-1]
ssh.logout()
return AsusWrtResult(neighbors, leases_result, arp_result,
nvram_result)
except pxssh.ExceptionPxssh as exc:
_LOGGER.error("Unexpected response from router: %s", exc)
return None
def telnet_connection(self):
"""Retrieve data from ASUSWRT via the telnet protocol."""
try:
telnet = telnetlib.Telnet(self.host)
telnet.read_until(b'login: ')
telnet.write((self.username + '\n').encode('ascii'))
telnet.read_until(b'Password: ')
telnet.write((self.password + '\n').encode('ascii'))
prompt_string = telnet.read_until(b'#').split(b'\n')[-1]
telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii'))
neighbors = 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])
telnet.write('{}\n'.format(_NVRAM_CMD).encode('ascii'))
nvram_result = (telnet.read_until(prompt_string).
split(b'\n')[1].split(b'<')[1:])
else:
arp_result = ['']
nvram_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 AsusWrtResult(neighbors, leases_result, arp_result,
nvram_result)
except EOFError:
_LOGGER.error("Unexpected response from router")
return None
except ConnectionRefusedError:
_LOGGER.error("Connection refused by router. 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 == 'ssh':
result = self.ssh_connection()
elif self.protocol == 'telnet':
result = self.telnet_connection()
else:
# autodetect protocol
result = self.ssh_connection()
if result:
self.protocol = 'ssh'
else:
result = self.telnet_connection()
if result:
self.protocol = 'telnet'
result = self.connection.get_result()
if not result:
return {}
@@ -363,3 +271,193 @@ class AsusWrtDeviceScanner(DeviceScanner):
if match.group('ip') in devices:
devices[match.group('ip')]['status'] = match.group('status')
return devices
class _Connection:
def __init__(self):
self._connected = False
@property
def connected(self):
"""Return connection state."""
return self._connected
def connect(self):
"""Mark currenct connection state as connected."""
self._connected = True
def disconnect(self):
"""Mark current connection state as disconnected."""
self._connected = False
class SshConnection(_Connection):
"""Maintains an SSH connection to an ASUS-WRT router."""
def __init__(self, host, port, username, password, ssh_key, ap):
"""Initialize the SSH connection properties."""
super(SshConnection, self).__init__()
self._ssh = None
self._host = host
self._port = port
self._username = username
self._password = password
self._ssh_key = ssh_key
self._ap = ap
def get_result(self):
"""Retrieve a single AsusWrtResult through an SSH connection.
Connect to the SSH server if not currently connected, otherwise
use the existing connection.
"""
from pexpect import pxssh, exceptions
try:
if not self.connected:
self.connect()
self._ssh.sendline(_IP_NEIGH_CMD)
self._ssh.prompt()
neighbors = self._ssh.before.split(b'\n')[1:-1]
if self._ap:
self._ssh.sendline(_ARP_CMD)
self._ssh.prompt()
arp_result = self._ssh.before.split(b'\n')[1:-1]
self._ssh.sendline(_WL_CMD)
self._ssh.prompt()
leases_result = self._ssh.before.split(b'\n')[1:-1]
self._ssh.sendline(_NVRAM_CMD)
self._ssh.prompt()
nvram_result = self._ssh.before.split(b'\n')[1].split(b'<')[1:]
else:
arp_result = ['']
nvram_result = ['']
self._ssh.sendline(_LEASES_CMD)
self._ssh.prompt()
leases_result = self._ssh.before.split(b'\n')[1:-1]
return AsusWrtResult(neighbors, leases_result, arp_result,
nvram_result)
except exceptions.EOF as err:
_LOGGER.error("Connection refused. SSH enabled?")
self.disconnect()
return None
except pxssh.ExceptionPxssh as err:
_LOGGER.error("Unexpected SSH error: %s", str(err))
self.disconnect()
return None
except AssertionError as err:
_LOGGER.error("Connection to router unavailable: %s", str(err))
self.disconnect()
return None
def connect(self):
"""Connect to the ASUS-WRT SSH server."""
from pexpect import pxssh
self._ssh = pxssh.pxssh()
if self._ssh_key:
self._ssh.login(self._host, self._username,
ssh_key=self._ssh_key, port=self._port)
else:
self._ssh.login(self._host, self._username,
password=self._password, port=self._port)
super(SshConnection, self).connect()
def disconnect(self): \
# pylint: disable=broad-except
"""Disconnect the current SSH connection."""
try:
self._ssh.logout()
except Exception:
pass
finally:
self._ssh = None
super(SshConnection, self).disconnect()
class TelnetConnection(_Connection):
"""Maintains a Telnet connection to an ASUS-WRT router."""
def __init__(self, host, port, username, password, ap):
"""Initialize the Telnet connection properties."""
super(TelnetConnection, self).__init__()
self._telnet = None
self._host = host
self._port = port
self._username = username
self._password = password
self._ap = ap
self._prompt_string = None
def get_result(self):
"""Retrieve a single AsusWrtResult through a Telnet connection.
Connect to the Telnet server if not currently connected, otherwise
use the existing connection.
"""
try:
if not self.connected:
self.connect()
self._telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii'))
neighbors = (self._telnet.read_until(self._prompt_string).
split(b'\n')[1:-1])
if self._ap:
self._telnet.write('{}\n'.format(_ARP_CMD).encode('ascii'))
arp_result = (self._telnet.read_until(self._prompt_string).
split(b'\n')[1:-1])
self._telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
leases_result = (self._telnet.read_until(self._prompt_string).
split(b'\n')[1:-1])
self._telnet.write('{}\n'.format(_NVRAM_CMD).encode('ascii'))
nvram_result = (self._telnet.read_until(self._prompt_string).
split(b'\n')[1].split(b'<')[1:])
else:
arp_result = ['']
nvram_result = ['']
self._telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
leases_result = (self._telnet.read_until(self._prompt_string).
split(b'\n')[1:-1])
return AsusWrtResult(neighbors, leases_result, arp_result,
nvram_result)
except EOFError:
_LOGGER.error("Unexpected response from router")
self.disconnect()
return None
except ConnectionRefusedError:
_LOGGER.error("Connection refused by router. Telnet enabled?")
self.disconnect()
return None
except socket.gaierror as exc:
_LOGGER.error("Socket exception: %s", exc)
self.disconnect()
return None
except OSError as exc:
_LOGGER.error("OSError: %s", exc)
self.disconnect()
return None
def connect(self):
"""Connect to the ASUS-WRT Telnet server."""
self._telnet = telnetlib.Telnet(self._host)
self._telnet.read_until(b'login: ')
self._telnet.write((self._username + '\n').encode('ascii'))
self._telnet.read_until(b'Password: ')
self._telnet.write((self._password + '\n').encode('ascii'))
self._prompt_string = self._telnet.read_until(b'#').split(b'\n')[-1]
super(TelnetConnection, self).connect()
def disconnect(self): \
# pylint: disable=broad-except
"""Disconnect the current Telnet connection."""
try:
self._telnet.write('exit\n'.encode('ascii'))
except Exception:
pass
super(TelnetConnection, self).disconnect()

View File

@@ -21,7 +21,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
REQUIREMENTS = ['aioautomatic==0.3.1']
REQUIREMENTS = ['aioautomatic==0.4.0']
_LOGGER = logging.getLogger(__name__)
@@ -31,7 +31,8 @@ CONF_DEVICES = 'devices'
DEFAULT_TIMEOUT = 5
SCOPE = ['location', 'vehicle:profile', 'trip']
DEFAULT_SCOPE = ['location', 'vehicle:profile', 'trip']
FULL_SCOPE = DEFAULT_SCOPE + ['current_location']
ATTR_FUEL_LEVEL = 'fuel_level'
@@ -58,8 +59,17 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
client_session=async_get_clientsession(hass),
request_kwargs={'timeout': DEFAULT_TIMEOUT})
try:
session = yield from client.create_session_from_password(
SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD])
try:
session = yield from client.create_session_from_password(
FULL_SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD])
except aioautomatic.exceptions.ForbiddenError as exc:
if not str(exc).startswith("invalid_scope"):
raise exc
_LOGGER.info("Client not authorized for current_location scope. "
"location:updated events will not be received.")
session = yield from client.create_session_from_password(
DEFAULT_SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD])
data = AutomaticData(
hass, client, session, config[CONF_DEVICES], async_see)

View File

@@ -39,7 +39,7 @@ class GPSLoggerView(HomeAssistantView):
@asyncio.coroutine
def get(self, request):
"""Handle for GPSLogger message received as GET."""
res = yield from self._handle(request.app['hass'], request.GET)
res = yield from self._handle(request.app['hass'], request.query)
return res
@asyncio.coroutine
@@ -75,10 +75,10 @@ class GPSLoggerView(HomeAssistantView):
if 'activity' in data:
attrs['activity'] = data['activity']
yield from hass.loop.run_in_executor(
None, partial(self.see, dev_id=device,
gps=gps_location, battery=battery,
gps_accuracy=accuracy,
attributes=attrs))
yield from hass.async_add_job(
partial(self.see, dev_id=device,
gps=gps_location, battery=battery,
gps_accuracy=accuracy,
attributes=attrs))
return 'Setting location for {}'.format(device)

View File

@@ -22,7 +22,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
INTERFACES = 2
DEFAULT_TIMEOUT = 10
REQUIREMENTS = ['beautifulsoup4==4.5.3']
REQUIREMENTS = ['beautifulsoup4==4.6.0']
_LOGGER = logging.getLogger(__name__)

View File

@@ -41,7 +41,7 @@ class LocativeView(HomeAssistantView):
@asyncio.coroutine
def get(self, request):
"""Locative message received as GET."""
res = yield from self._handle(request.app['hass'], request.GET)
res = yield from self._handle(request.app['hass'], request.query)
return res
@asyncio.coroutine
@@ -79,10 +79,9 @@ class LocativeView(HomeAssistantView):
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
if direction == 'enter':
yield from hass.loop.run_in_executor(
None, partial(self.see, dev_id=device,
location_name=location_name,
gps=gps_location))
yield from hass.async_add_job(
partial(self.see, dev_id=device, location_name=location_name,
gps=gps_location))
return 'Setting location to {}'.format(location_name)
elif direction == 'exit':
@@ -91,10 +90,9 @@ class LocativeView(HomeAssistantView):
if current_state is None or current_state.state == location_name:
location_name = STATE_NOT_HOME
yield from hass.loop.run_in_executor(
None, partial(self.see, dev_id=device,
location_name=location_name,
gps=gps_location))
yield from hass.async_add_job(
partial(self.see, dev_id=device,
location_name=location_name, gps=gps_location))
return 'Setting location to not home'
else:
# Ignore the message if it is telling us to exit a zone that we

View File

@@ -60,13 +60,20 @@ class MikrotikScanner(DeviceScanner):
self.success_init = False
self.client = None
self.wireless_exist = None
self.success_init = self.connect_to_device()
if self.success_init:
_LOGGER.info("Start polling Mikrotik router...")
_LOGGER.info(
"Start polling Mikrotik (%s) router...",
self.host
)
self._update_info()
else:
_LOGGER.error("Connection to Mikrotik failed")
_LOGGER.error(
"Connection to Mikrotik (%s) failed",
self.host
)
def connect_to_device(self):
"""Connect to Mikrotik method."""
@@ -87,6 +94,16 @@ class MikrotikScanner(DeviceScanner):
routerboard_info[0].get('model', 'Router'),
self.host)
self.connected = True
self.wireless_exist = self.client(
cmd='/interface/wireless/getall'
)
if not self.wireless_exist:
_LOGGER.info(
'Mikrotik %s: Wireless adapters not found. Try to '
'use DHCP lease table as presence tracker source. '
'Please decrease lease time as much as possible.',
self.host
)
except (librouteros.exceptions.TrapError,
librouteros.exceptions.ConnectionError) as api_error:
@@ -108,24 +125,39 @@ class MikrotikScanner(DeviceScanner):
def _update_info(self):
"""Retrieve latest information from the Mikrotik box."""
with self.lock:
_LOGGER.info("Loading wireless device from Mikrotik...")
if self.wireless_exist:
devices_tracker = 'wireless'
else:
devices_tracker = 'ip'
wireless_clients = self.client(
cmd='/interface/wireless/registration-table/getall'
_LOGGER.info(
"Loading %s devices from Mikrotik (%s) ...",
devices_tracker,
self.host
)
device_names = self.client(cmd='/ip/dhcp-server/lease/getall')
if device_names is None or wireless_clients is None:
device_names = self.client(cmd='/ip/dhcp-server/lease/getall')
if self.wireless_exist:
devices = self.client(
cmd='/interface/wireless/registration-table/getall'
)
else:
devices = device_names
if device_names is None and devices is None:
return False
mac_names = {device.get('mac-address'): device.get('host-name')
for device in device_names
if device.get('mac-address')}
self.last_results = {
device.get('mac-address'):
mac_names.get(device.get('mac-address'))
for device in wireless_clients
}
if self.wireless_exist:
self.last_results = {
device.get('mac-address'):
mac_names.get(device.get('mac-address'))
for device in devices
}
else:
self.last_results = mac_names
return True

View File

@@ -19,7 +19,7 @@ from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pysnmp==4.3.5']
REQUIREMENTS = ['pysnmp==4.3.7']
CONF_COMMUNITY = 'community'
CONF_AUTHKEY = 'authkey'

View File

@@ -163,6 +163,7 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
self._log_out()
return self.last_results.keys()
# pylint: disable=no-self-use
@@ -195,8 +196,9 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
self.sysauth = regex_result.group(1)
_LOGGER.info(self.sysauth)
return True
except ValueError:
_LOGGER.error("Couldn't fetch auth tokens!")
except (ValueError, KeyError) as _:
_LOGGER.error("Couldn't fetch auth tokens! Response was: %s",
response.text)
return False
@Throttle(MIN_TIME_BETWEEN_SCANS)
@@ -250,6 +252,21 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
return False
def _log_out(self):
with self.lock:
_LOGGER.info("Logging out of router admin interface...")
url = ('http://{}/cgi-bin/luci/;stok={}/admin/system?'
'form=logout').format(self.host, self.stok)
referer = 'http://{}/webpages/index.html'.format(self.host)
requests.post(url,
params={'operation': 'write'},
headers={'referer': referer},
cookies={'sysauth': self.sysauth})
self.stok = ''
self.sysauth = ''
class Tplink4DeviceScanner(TplinkDeviceScanner):
"""This class queries an Archer C7 router with TP-Link firmware 150427."""

5
homeassistant/components/device_tracker/ubus.py Normal file → Executable file
View File

@@ -144,7 +144,10 @@ def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params):
response = res.json()
if rpcmethod == "call":
return response["result"][1]
try:
return response["result"][1]
except IndexError:
return
else:
return response["result"]

View File

@@ -15,7 +15,7 @@ from homeassistant.components.device_tracker import (
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.const import CONF_VERIFY_SSL
REQUIREMENTS = ['pyunifi==2.0']
REQUIREMENTS = ['pyunifi==2.12']
_LOGGER = logging.getLogger(__name__)
CONF_PORT = 'port'

View File

@@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==1.0.0rc3']
REQUIREMENTS = ['netdisco==1.0.1']
DOMAIN = 'discovery'
@@ -31,6 +31,7 @@ SERVICE_WEMO = 'belkin_wemo'
SERVICE_HASS_IOS_APP = 'hass_ios'
SERVICE_IKEA_TRADFRI = 'ikea_tradfri'
SERVICE_HASSIO = 'hassio'
SERVICE_AXIS = 'axis'
SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None),
@@ -38,6 +39,7 @@ SERVICE_HANDLERS = {
SERVICE_WEMO: ('wemo', None),
SERVICE_IKEA_TRADFRI: ('tradfri', None),
SERVICE_HASSIO: ('hassio', None),
SERVICE_AXIS: ('axis', None),
'philips_hue': ('light', 'hue'),
'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'),
@@ -113,8 +115,7 @@ def async_setup(hass, config):
@asyncio.coroutine
def scan_devices(now):
"""Scan for devices."""
results = yield from hass.loop.run_in_executor(
None, _discover, netdisco)
results = yield from hass.async_add_job(_discover, netdisco)
for result in results:
hass.async_add_job(new_service_found(*result))

View File

@@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import state as state_helper
from homeassistant.util import Throttle
REQUIREMENTS = ['dweepy==0.2.0']
REQUIREMENTS = ['dweepy==0.3.0']
_LOGGER = logging.getLogger(__name__)
@@ -67,4 +67,4 @@ def send_data(name, msg):
try:
dweepy.dweet_for(name, msg)
except dweepy.DweepyError:
_LOGGER.error("Error saving data '%s' to Dweet.io", msg)
_LOGGER.error("Error saving data to Dweet.io: %s", msg)

View File

@@ -24,7 +24,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
REQUIREMENTS = ['pyeight==0.0.4']
REQUIREMENTS = ['pyeight==0.0.6']
_LOGGER = logging.getLogger(__name__)
@@ -145,6 +145,9 @@ def async_setup(hass, config):
sensors.append('{}_{}'.format(obj.side, sensor))
binary_sensors.append('{}_presence'.format(obj.side))
sensors.append('room_temp')
else:
# No users, cannot continue
return False
hass.async_add_job(discovery.async_load_platform(
hass, 'sensor', DOMAIN, {
@@ -156,8 +159,8 @@ def async_setup(hass, config):
CONF_BINARY_SENSORS: binary_sensors,
}, config))
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file,
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
REQUIREMENTS = ['pyenvisalink==2.0']
REQUIREMENTS = ['pyenvisalink==2.1']
_LOGGER = logging.getLogger(__name__)

View File

@@ -229,8 +229,8 @@ def async_setup(hass, config: dict):
yield from asyncio.wait(update_tasks, loop=hass.loop)
# Listen for fan service calls.
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
for service_name in SERVICE_TO_METHOD:
@@ -256,7 +256,7 @@ class FanEntity(ToggleEntity):
"""
if speed is SPEED_OFF:
return self.async_turn_off()
return self.hass.loop.run_in_executor(None, self.set_speed, speed)
return self.hass.async_add_job(self.set_speed, speed)
def set_direction(self: ToggleEntity, direction: str) -> None:
"""Set the direction of the fan."""
@@ -267,8 +267,7 @@ class FanEntity(ToggleEntity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.set_direction, direction)
return self.hass.async_add_job(self.set_direction, direction)
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
"""Turn on the fan."""
@@ -281,8 +280,8 @@ class FanEntity(ToggleEntity):
"""
if speed is SPEED_OFF:
return self.async_turn_off()
return self.hass.loop.run_in_executor(
None, ft.partial(self.turn_on, speed, **kwargs))
return self.hass.async_add_job(
ft.partial(self.turn_on, speed, **kwargs))
def oscillate(self: ToggleEntity, oscillating: bool) -> None:
"""Oscillate the fan."""
@@ -293,8 +292,7 @@ class FanEntity(ToggleEntity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.oscillate, oscillating)
return self.hass.async_add_job(self.oscillate, oscillating)
@property
def is_on(self):

View File

@@ -4,6 +4,7 @@ Support for Wink fans.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/fan.wink/
"""
import asyncio
import logging
from homeassistant.components.fan import (FanEntity, SPEED_HIGH,
@@ -12,6 +13,8 @@ from homeassistant.components.fan import (FanEntity, SPEED_HIGH,
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.wink import WinkDevice, DOMAIN
DEPENDENCIES = ['wink']
_LOGGER = logging.getLogger(__name__)
SPEED_LOWEST = 'lowest'
@@ -34,6 +37,11 @@ class WinkFanDevice(WinkDevice, FanEntity):
"""Initialize the fan."""
super().__init__(wink, hass)
@asyncio.coroutine
def async_added_to_hass(self):
"""Callback when entity is added to hass."""
self.hass.data[DOMAIN]['entities']['fan'].append(self)
def set_direction(self: ToggleEntity, direction: str) -> None:
"""Set the direction of the fan."""
self.wink.set_fan_direction(direction)

View File

@@ -0,0 +1,86 @@
"""
Z-Wave platform that handles fans.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/fan.zwave/
"""
import logging
import math
from homeassistant.components.fan import (
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED)
from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
_LOGGER = logging.getLogger(__name__)
SPEED_LIST = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
SUPPORTED_FEATURES = SUPPORT_SET_SPEED
# Value will first be divided to an integer
VALUE_TO_SPEED = {
0: SPEED_OFF,
1: SPEED_LOW,
2: SPEED_MEDIUM,
3: SPEED_HIGH,
}
SPEED_TO_VALUE = {
SPEED_OFF: 0,
SPEED_LOW: 1,
SPEED_MEDIUM: 50,
SPEED_HIGH: 99,
}
def get_device(values, **kwargs):
"""Create zwave entity device."""
return ZwaveFan(values)
class ZwaveFan(zwave.ZWaveDeviceEntity, FanEntity):
"""Representation of a Z-Wave fan."""
def __init__(self, values):
"""Initialize the Z-Wave fan device."""
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
self.update_properties()
def update_properties(self):
"""Handle data changes for node values."""
value = math.ceil(self.values.primary.data * 3 / 100)
self._state = VALUE_TO_SPEED[value]
def set_speed(self, speed):
"""Set the speed of the fan."""
self.node.set_dimmer(
self.values.primary.value_id, SPEED_TO_VALUE[speed])
def turn_on(self, speed=None, **kwargs):
"""Turn the device on."""
if speed is None:
# Value 255 tells device to return to previous value
self.node.set_dimmer(self.values.primary.value_id, 255)
else:
self.set_speed(speed)
def turn_off(self, **kwargs):
"""Turn the device off."""
self.node.set_dimmer(self.values.primary.value_id, 0)
@property
def speed(self):
"""Return the current speed."""
return self._state
@property
def speed_list(self):
"""Get the list of available speeds."""
return SPEED_LIST
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORTED_FEATURES

View File

@@ -89,8 +89,8 @@ def async_setup(hass, config):
conf.get(CONF_RUN_TEST, DEFAULT_RUN_TEST)
)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file,
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml'))
# Register service

View File

@@ -268,8 +268,8 @@ class IndexView(HomeAssistantView):
no_auth = 'true'
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
template = yield from hass.loop.run_in_executor(
None, self.templates.get_template, 'index.html')
template = yield from hass.async_add_job(
self.templates.get_template, 'index.html')
# pylint is wrong
# pylint: disable=no-member

View File

@@ -1,22 +1,23 @@
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
FINGERPRINTS = {
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
"core.js": "5d08475f03adb5969bd31855d5ca0cfd",
"frontend.html": "5999c8fac69c503b846672cae75a12b0",
"compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812",
"core.js": "d4a7cb8c80c62b536764e0e81385f6aa",
"frontend.html": "ed18c05632c071eb4f7b012382d0f810",
"mdi.html": "f407a5a57addbe93817ee1b244d33fbe",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-automation.html": "21cba0a4fee9d2b45dda47f7a1dd82d8",
"panels/ha-panel-config.html": "59d9eb28758b497a4d9b2428f978b9b1",
"panels/ha-panel-dev-event.html": "2db9c218065ef0f61d8d08db8093cad2",
"panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d",
"panels/ha-panel-dev-service.html": "415552027cb083badeff5f16080410ed",
"panels/ha-panel-dev-state.html": "d70314913b8923d750932367b1099750",
"panels/ha-panel-dev-template.html": "567fbf86735e1b891e40c2f4060fec9b",
"panels/ha-panel-hassio.html": "333f86e5f516b31e52365e412deb7fdc",
"panels/ha-panel-hassio.html": "9474ba65077371622f21ed9a30cf5229",
"panels/ha-panel-history.html": "89062c48c76206cad1cec14ddbb1cbb1",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163",
"panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4",
"panels/ha-panel-zwave.html": "84fb45638d2a69bac343246a687f647c",
"panels/ha-panel-zwave.html": "780a792213e98510b475f752c40ef0f9",
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
}

View File

@@ -1 +1 @@
!(function(){"use strict";function e(e,r){var t=arguments;if(void 0===e||null===e)throw new TypeError("Cannot convert first argument to object");for(var n=Object(e),o=1;o<arguments.length;o++){var i=t[o];if(void 0!==i&&null!==i)for(var l=Object.keys(Object(i)),a=0,c=l.length;a<c;a++){var b=l[a],f=Object.getOwnPropertyDescriptor(i,b);void 0!==f&&f.enumerable&&(n[b]=i[b])}}return n}function r(){Object.assign||Object.defineProperty(Object,"assign",{enumerable:!1,configurable:!0,writable:!0,value:e})}var t={assign:e,polyfill:r};t.polyfill()})();
!function(){"use strict";function e(e,t){if(void 0===e||null===e)throw new TypeError("Cannot convert first argument to object");for(var r=Object(e),n=1;n<arguments.length;n++){var o=arguments[n];if(void 0!==o&&null!==o)for(var i=Object.keys(Object(o)),l=0,c=i.length;l<c;l++){var a=i[l],b=Object.getOwnPropertyDescriptor(o,a);void 0!==b&&b.enumerable&&(r[a]=o[a])}}return r}function t(){Object.assign||Object.defineProperty(Object,"assign",{enumerable:!1,configurable:!0,writable:!0,value:e})}({assign:e,polyfill:t}).polyfill()}();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -173,8 +173,8 @@ def async_setup(hass, config):
yield from _async_process_config(hass, config, component)
descriptions = yield from hass.loop.run_in_executor(
None, conf_util.load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
conf_util.load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml')
)

View File

@@ -16,7 +16,7 @@ from aiohttp.hdrs import CONTENT_TYPE
import async_timeout
from homeassistant.const import CONTENT_TYPE_TEXT_PLAIN
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.components.frontend import register_built_in_panel
@@ -139,7 +139,7 @@ class HassIOView(HomeAssistantView):
name = "api:hassio"
url = "/api/hassio/{path:.+}"
requires_auth = True
requires_auth = False
def __init__(self, hassio):
"""Initialize a hassio base view."""
@@ -148,6 +148,9 @@ class HassIOView(HomeAssistantView):
@asyncio.coroutine
def _handle(self, request, path):
"""Route data to hassio."""
if path != 'panel' and not request[KEY_AUTHENTICATED]:
return web.Response(status=401)
client = yield from self.hassio.command_proxy(path, request)
data = yield from client.read()

View File

@@ -223,7 +223,7 @@ class HistoryPeriodView(HomeAssistantView):
if start_time > now:
return self.json([])
end_time = request.GET.get('end_time')
end_time = request.query.get('end_time')
if end_time:
end_time = dt_util.as_utc(
dt_util.parse_datetime(end_time))
@@ -231,11 +231,11 @@ class HistoryPeriodView(HomeAssistantView):
return self.json_message('Invalid end_time', HTTP_BAD_REQUEST)
else:
end_time = start_time + one_day
entity_id = request.GET.get('filter_entity_id')
entity_id = request.query.get('filter_entity_id')
result = yield from request.app['hass'].loop.run_in_executor(
None, get_significant_states, request.app['hass'], start_time,
end_time, entity_id, self.filters)
result = yield from request.app['hass'].async_add_job(
get_significant_states, request.app['hass'], start_time, end_time,
entity_id, self.filters)
result = result.values()
if _LOGGER.isEnabledFor(logging.DEBUG):
elapsed = time.perf_counter() - timer_start

View File

@@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_time_interval
from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['pyhomematic==0.1.25']
REQUIREMENTS = ['pyhomematic==0.1.27']
DOMAIN = 'homematic'

View File

@@ -37,8 +37,8 @@ def auth_middleware(app, handler):
# A valid auth header has been set
authenticated = True
elif (DATA_API_PASSWORD in request.GET and
validate_password(request, request.GET[DATA_API_PASSWORD])):
elif (DATA_API_PASSWORD in request.query and
validate_password(request, request.query[DATA_API_PASSWORD])):
authenticated = True
elif is_trusted_ip(request):

View File

@@ -40,8 +40,8 @@ def ban_middleware(app, handler):
if KEY_BANNED_IPS not in app:
hass = app['hass']
app[KEY_BANNED_IPS] = yield from hass.loop.run_in_executor(
None, load_ip_bans_config, hass.config.path(IP_BANS_FILE))
app[KEY_BANNED_IPS] = yield from hass.async_add_job(
load_ip_bans_config, hass.config.path(IP_BANS_FILE))
@asyncio.coroutine
def ban_middleware_handler(request):
@@ -90,9 +90,8 @@ def process_wrong_login(request):
request.app[KEY_BANNED_IPS].append(new_ban)
hass = request.app['hass']
yield from hass.loop.run_in_executor(
None, update_ip_bans_config, hass.config.path(IP_BANS_FILE),
new_ban)
yield from hass.async_add_job(
update_ip_bans_config, hass.config.path(IP_BANS_FILE), new_ban)
_LOGGER.warning(
"Banned IP %s for too many login attempts", remote_addr)

View File

@@ -11,25 +11,26 @@ import os
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_NAME, CONF_ENTITY_ID)
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.loader import get_component
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'image_processing'
DEPENDENCIES = ['camera']
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=10)
DEVICE_CLASSES = [
'alpr', # automatic license plate recognition
'face', # face
'alpr', # Automatic license plate recognition
'face', # Face
'ocr', # OCR
]
SERVICE_SCAN = 'scan'
@@ -71,8 +72,8 @@ def async_setup(hass, config):
yield from component.async_setup(config)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file,
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine
@@ -116,7 +117,7 @@ class ImageProcessingEntity(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(None, self.process_image, image)
return self.hass.async_add_job(self.process_image, image)
@asyncio.coroutine
def async_update(self):

View File

@@ -1,7 +1,7 @@
"""
Support for the demo image processing.
For more details about this component, please refer to the documentation at
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/demo/
"""
from homeassistant.components.image_processing import ATTR_CONFIDENCE
@@ -12,7 +12,7 @@ from homeassistant.components.image_processing.microsoft_face_identify import (
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the demo image_processing platform."""
"""Set up the demo image processing platform."""
add_devices([
DemoImageProcessingAlpr('camera.demo_camera', "Demo Alpr"),
DemoImageProcessingFace(
@@ -21,10 +21,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class DemoImageProcessingAlpr(ImageProcessingAlprEntity):
"""Demo alpr image processing entity."""
"""Demo ALPR image processing entity."""
def __init__(self, camera_entity, name):
"""Initialize demo alpr."""
"""Initialize demo ALPR image processing entity."""
super().__init__()
self._name = name
@@ -61,7 +61,7 @@ class DemoImageProcessingFace(ImageProcessingFaceEntity):
"""Demo face identify image processing entity."""
def __init__(self, camera_entity, name):
"""Initialize demo alpr."""
"""Initialize demo face image processing entity."""
super().__init__()
self._name = name

View File

@@ -1,7 +1,7 @@
"""
Component that will help set the dlib face detect processing.
Component that will help set the Dlib face detect processing.
For more details about this component, please refer to the documentation at
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/image_processing.dlib_face_detect/
"""
import logging
@@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Microsoft Face detection platform."""
"""Set up the Dlib Face detection platform."""
entities = []
for camera in config[CONF_SOURCE]:
entities.append(DlibFaceDetectEntity(
@@ -35,7 +35,7 @@ class DlibFaceDetectEntity(ImageProcessingFaceEntity):
"""Dlib Face API entity for identify."""
def __init__(self, camera_entity, name=None):
"""Initialize Dlib."""
"""Initialize Dlib face entity."""
super().__init__()
self._camera = camera_entity
@@ -62,7 +62,7 @@ class DlibFaceDetectEntity(ImageProcessingFaceEntity):
import face_recognition
fak_file = io.BytesIO(image)
fak_file.name = "snapshot.jpg"
fak_file.name = 'snapshot.jpg'
fak_file.seek(0)
image = face_recognition.load_image_file(fak_file)

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