Compare commits

...

124 Commits

Author SHA1 Message Date
Paulus Schoutsen c2b7c93375 Merge pull request #7968 from home-assistant/release-0-46-1
0.46.1
2017-06-08 22:20:32 -07:00
Paulus Schoutsen 8cc759ea4b Prevent Roku doing I/O in event loop (#7969) 2017-06-08 22:18:48 -07:00
Jacob Mansfield c32807803e Create metoffice.py (#7965)
Fix met office sensor
2017-06-08 21:44:33 -07:00
Barry Williams 372169a03a Fixed metadata issue (#7932) 2017-06-08 21:41:42 -07:00
cribbstechnologies bfd9623d8b Mqtt cover modifications (#7841)
* adding set position ability
removing command_topic being required

* flaking

* flaking test

* updating docs

* requested updates

* Revert "updating docs"

This reverts commit 9cfc5ed7a8.

* forgot to update constructor calls in tests
2017-06-08 21:35:38 -07:00
mje-nz 3464454662 Fix typos in Wunderground component (Percipitation -> Precipitation) (#7901) 2017-06-08 21:35:26 -07:00
Johan Bloemberg 533bb5565b Dsmr5 revert (#7900)
* Revert "Update to dsmr_parser supporting v5 arguments."

This reverts commit 3567de4b90.

* Revert "Using dev branch until released upstream."

This reverts commit 53e8de112c.

* Revert "Give good example."

This reverts commit 4f90fc4be6.

* Revert "Allow configuring DSMR5 protocol."

This reverts commit 9fa0e14187.
2017-06-08 21:35:26 -07:00
Adam Mills a8709a6988 Support for renaming ZWave values (#7780)
* Support for renaming ZWave values

* Improve test
2017-06-08 21:35:26 -07:00
Paulus Schoutsen 4b767b088e Version bump to 0.46.1 2017-06-08 21:34:39 -07:00
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
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 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 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
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
212 changed files with 6300 additions and 2922 deletions
+6
View File
@@ -20,6 +20,9 @@ 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
@@ -89,6 +92,9 @@ 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
-1
View File
@@ -5,7 +5,6 @@ MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
#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
+4 -6
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>
+11 -1
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):
+5 -7
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
@@ -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):
@@ -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."""
@@ -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(
+2 -2
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, {})
+1 -1
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__)
+1 -1
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]
+60
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
@@ -158,8 +158,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')
)
+8 -5
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,12 +77,13 @@ def async_trigger(hass, config, action):
}
})
if time_delta is None:
call_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 only state attributes changed, ignore this event
if from_s.last_changed == to_s.last_changed:
if time_delta is None:
call_action()
return
@callback
+11 -5
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)
@@ -38,7 +38,7 @@ class MyStromView(HomeAssistantView):
@asyncio.coroutine
def get(self, request):
"""The GET request received from a myStrom button."""
res = yield from self._handle(request.app['hass'], request.GET)
res = yield from self._handle(request.app['hass'], request.query)
return res
@asyncio.coroutine
+82 -82
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)
+78 -78
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
+2 -2
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)
+92
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
+2 -2
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:
+2 -2
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)
+250 -250
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
+13 -22
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):
+1 -1
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
+15 -18
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))
+41 -5
View File
@@ -14,7 +14,9 @@ import homeassistant.components.mqtt as mqtt
from homeassistant.components.cover import (
CoverDevice, ATTR_TILT_POSITION, SUPPORT_OPEN_TILT,
SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT, SUPPORT_SET_TILT_POSITION,
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, SUPPORT_SET_POSITION)
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, SUPPORT_SET_POSITION,
ATTR_POSITION)
from homeassistant.exceptions import TemplateError
from homeassistant.const import (
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN,
STATE_CLOSED, STATE_UNKNOWN)
@@ -29,6 +31,8 @@ DEPENDENCIES = ['mqtt']
CONF_TILT_COMMAND_TOPIC = 'tilt_command_topic'
CONF_TILT_STATUS_TOPIC = 'tilt_status_topic'
CONF_POSITION_TOPIC = 'set_position_topic'
CONF_SET_POSITION_TEMPLATE = 'set_position_template'
CONF_PAYLOAD_OPEN = 'payload_open'
CONF_PAYLOAD_CLOSE = 'payload_close'
@@ -55,10 +59,17 @@ DEFAULT_TILT_MAX = 100
DEFAULT_TILT_OPTIMISTIC = False
DEFAULT_TILT_INVERT_STATE = False
OPEN_CLOSE_FEATURES = (SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP)
TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT |
SUPPORT_SET_TILT_POSITION)
PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_COMMAND_TOPIC, default=None): valid_publish_topic,
vol.Optional(CONF_POSITION_TOPIC, default=None): valid_publish_topic,
vol.Optional(CONF_SET_POSITION_TEMPLATE, default=None): cv.template,
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string,
vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string,
@@ -87,6 +98,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = hass
set_position_template = config.get(CONF_SET_POSITION_TEMPLATE)
if set_position_template is not None:
set_position_template.hass = hass
async_add_devices([MqttCover(
config.get(CONF_NAME),
@@ -109,6 +123,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
config.get(CONF_TILT_MAX),
config.get(CONF_TILT_STATE_OPTIMISTIC),
config.get(CONF_TILT_INVERT_STATE),
config.get(CONF_POSITION_TOPIC),
set_position_template,
)])
@@ -120,7 +136,7 @@ class MqttCover(CoverDevice):
payload_open, payload_close, payload_stop,
optimistic, value_template, tilt_open_position,
tilt_closed_position, tilt_min, tilt_max, tilt_optimistic,
tilt_invert):
tilt_invert, position_topic, set_position_template):
"""Initialize the cover."""
self._position = None
self._state = None
@@ -145,6 +161,8 @@ class MqttCover(CoverDevice):
self._tilt_max = tilt_max
self._tilt_optimistic = tilt_optimistic
self._tilt_invert = tilt_invert
self._position_topic = position_topic
self._set_position_template = set_position_template
@asyncio.coroutine
def async_added_to_hass(self):
@@ -233,9 +251,11 @@ class MqttCover(CoverDevice):
@property
def supported_features(self):
"""Flag supported features."""
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
supported_features = 0
if self._command_topic is not None:
supported_features = OPEN_CLOSE_FEATURES
if self.current_cover_position is not None:
if self._position_topic is not None:
supported_features |= SUPPORT_SET_POSITION
if self._tilt_command_topic is not None:
@@ -315,6 +335,22 @@ class MqttCover(CoverDevice):
mqtt.async_publish(self.hass, self._tilt_command_topic,
level, self._qos, self._retain)
@asyncio.coroutine
def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
if ATTR_POSITION in kwargs:
position = kwargs[ATTR_POSITION]
if self._set_position_template is not None:
try:
position = self._set_position_template.async_render(
**kwargs)
except TemplateError as ex:
_LOGGER.error(ex)
self._state = None
mqtt.async_publish(self.hass, self._position_topic,
position, 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
+26 -15
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):
+1 -1
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
@@ -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,
+203 -105
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()
@@ -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)
@@ -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
@@ -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
@@ -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'
+4 -1
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"]
+2 -3
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.0']
REQUIREMENTS = ['netdisco==1.0.1']
DOMAIN = 'discovery'
@@ -115,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))
+3 -3
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.5']
REQUIREMENTS = ['pyeight==0.0.6']
_LOGGER = logging.getLogger(__name__)
@@ -159,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
+1 -1
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__)
+7 -9
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):
+86
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
+2 -2
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
@@ -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
+2 -2
View File
@@ -3,7 +3,7 @@
FINGERPRINTS = {
"compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812",
"core.js": "d4a7cb8c80c62b536764e0e81385f6aa",
"frontend.html": "fbb9d6bdd3d661db26cad9475a5e22f1",
"frontend.html": "ed18c05632c071eb4f7b012382d0f810",
"mdi.html": "f407a5a57addbe93817ee1b244d33fbe",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-automation.html": "21cba0a4fee9d2b45dda47f7a1dd82d8",
@@ -18,6 +18,6 @@ FINGERPRINTS = {
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163",
"panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4",
"panels/ha-panel-zwave.html": "19336d2c50c91dd6a122acc0606ff10d",
"panels/ha-panel-zwave.html": "780a792213e98510b475f752c40ef0f9",
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
}
File diff suppressed because one or more lines are too long
@@ -31,6 +31,200 @@
});
this.selectedNodeAttrs = att.sort();
},
});</script><dom-module id="zwave-values" assetpath="./"><template><style include="iron-flex ha-style">.content{margin-top:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}.help-text{padding-left:24px;padding-right:24px}</style><div class="content"><paper-card heading="Node Values"><div class="device-picker"><paper-dropdown-menu label="Value" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedValue}}"><template is="dom-repeat" items="[[values]]" as="item"><paper-item>[[computeSelectCaption(item)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsValueSelected(selectedValue)]]"><paper-input float-label="Value Name" type="text" value="{{newValueNameInput}}" placeholder="[[computeGetValueName(selectedValue)]]"></paper-input><ha-call-service-button hass="[[hass]]" domain="zwave" service="rename_value" service-data="[[computeValueNameServiceData(newValueNameInput)]]">Rename Value</ha-call-service-button></template></paper-card></div></template></dom-module><script>Polymer({
is: 'zwave-values',
properties: {
hass: {
type: Object,
},
nodes: {
type: Array,
},
values: {
type: Array,
},
selectedNode: {
type: Number,
},
selectedValue: {
type: Number,
value: -1,
},
},
listeners: {
'hass-service-called': 'serviceCalled',
},
serviceCalled: function (ev) {
if (ev.detail.success) {
var foo = this;
setTimeout(function () {
foo.refreshValues(foo.selectedNode);
}, 5000);
}
},
computeSelectCaption: function (item) {
return item.value.label;
},
computeGetValueName: function (selectedValue) {
return this.values[selectedValue].value.label;
},
computeIsValueSelected: function (selectedValue) {
return (!this.nodes || this.selectedNode === -1 || selectedValue === -1);
},
refreshValues: function (selectedNode) {
var valueData = [];
this.hass.callApi('GET', 'zwave/values/' + this.nodes[selectedNode].attributes.node_id).then(function (values) {
Object.entries(values).forEach(([key, value]) => {
valueData.push({ key, value });
});
this.values = valueData;
this.selectedValueChanged(this.selectedValue);
}.bind(this));
},
computeValueNameServiceData: function (newValueNameInput) {
if (!this.selectedNode === -1 || this.selectedValue === -1) return -1;
return {
node_id: this.nodes[this.selectedNode].attributes.node_id,
value_id: this.values[this.selectedValue].key,
name: newValueNameInput,
};
},
});</script><dom-module id="zwave-groups" assetpath="./"><template><style include="iron-flex ha-style">.content{margin-top:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}.help-text{padding-left:24px;padding-right:24px}</style><div class="content"><paper-card heading="Node group associations"><div class="device-picker"><paper-dropdown-menu label="Node to control" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedTargetNode}}"><template is="dom-repeat" items="[[nodes]]" as="state"><paper-item>[[computeSelectCaption(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsTargetNodeSelected(selectedTargetNode)]]"><div class="device-picker"><paper-dropdown-menu label="Group" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedGroup}}"><template is="dom-repeat" items="[[groups]]" as="state"><paper-item>[[computeSelectCaptionGroup(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div></template><template is="dom-if" if="[[!computeIsGroupSelected(selectedGroup)]]"><div class="help-text"><span>Other Nodes in this group:</span><template is="dom-repeat" items="[[otherGroupNodes]]" as="state"><span>[[state]]</span></template></div><div class="help-text"><span>Max Associations:</span> <span>[[maxAssociations]]</span></div><div class="card-actions"><template is="dom-if" if="[[!noAssociationsLeft]]"><ha-call-service-button hass="[[hass]]" domain="zwave" service="change_association" service-data="[[computeAssocServiceData(selectedGroup, &quot;add&quot;)]]">Add To Group</ha-call-service-button></template><ha-call-service-button hass="[[hass]]" domain="zwave" service="change_association" service-data="[[computeAssocServiceData(selectedGroup, &quot;remove&quot;)]]">Remove From Group</ha-call-service-button></div></template></paper-card></div></template></dom-module><script>Polymer({
is: 'zwave-groups',
properties: {
hass: {
type: Object,
},
nodes: {
type: Array,
},
groups: {
type: Array,
},
selectedNode: {
type: Number,
},
selectedTargetNode: {
type: Number,
value: -1
},
selectedGroup: {
type: Number,
value: -1,
observer: 'selectedGroupChanged'
},
otherGroupNodes: {
type: Array,
value: -1,
computed: 'computeOtherGroupNodes(selectedGroup)'
},
maxAssociations: {
type: String,
value: '',
computed: 'computeMaxAssociations(selectedGroup)'
},
noAssociationsLeft: {
type: Boolean,
value: true,
computed: 'computeAssociationsLeft(selectedGroup)'
},
},
listeners: {
'hass-service-called': 'serviceCalled',
},
serviceCalled: function (ev) {
if (ev.detail.success) {
var foo = this;
setTimeout(function () {
foo.refreshGroups(foo.selectedNode);
}, 5000);
}
},
computeAssociationsLeft: function (selectedGroup) {
if (selectedGroup === -1) return true;
return (this.maxAssociations === this.otherGroupNodes.length);
},
computeMaxAssociations: function (selectedGroup) {
if (selectedGroup === -1) return -1;
var maxAssociations = this.groups[selectedGroup].value.max_associations;
if (!maxAssociations) return ['None'];
return maxAssociations;
},
computeOtherGroupNodes: function (selectedGroup) {
if (selectedGroup === -1) return -1;
var associations = Object.values(this.groups[selectedGroup].value.associations);
if (!associations.length) return ['None'];
return associations;
},
computeSelectCaption: function (stateObj) {
return window.hassUtil.computeStateName(stateObj) + ' (Node:' +
stateObj.attributes.node_id + ' ' +
stateObj.attributes.query_stage + ')';
},
computeSelectCaptionGroup: function (stateObj) {
return (stateObj.key + ': ' + stateObj.value.label);
},
computeIsTargetNodeSelected: function (selectedTargetNode) {
return (!this.nodes || selectedTargetNode === -1);
},
computeIsGroupSelected: function (selectedGroup) {
return (!this.nodes || this.selectedNode === -1 || selectedGroup === -1);
},
computeAssocServiceData: function (selectedGroup, type) {
if (!this.groups === -1 || selectedGroup === -1 || this.selectedNode === -1) return -1;
return { node_id: this.nodes[this.selectedNode].attributes.node_id,
association: type,
target_node_id: this.nodes[this.selectedTargetNode].attributes.node_id,
group: this.groups[selectedGroup].key };
},
refreshGroups: function (selectedNode) {
var groupData = [];
this.hass.callApi('GET', 'zwave/groups/' + this.nodes[selectedNode].attributes.node_id).then(function (groups) {
Object.entries(groups).forEach(([key, value]) => {
groupData.push({ key, value });
});
this.groups = groupData;
this.selectedGroupChanged(this.selectedGroup);
}.bind(this));
},
selectedGroupChanged: function (selectedGroup) {
if (this.selectedGroup === -1 || selectedGroup === -1) return;
this.maxAssociations = this.groups[selectedGroup].value.max_associations;
this.otherGroupNodes = Object.values(this.groups[selectedGroup].value.associations);
},
});</script><dom-module id="zwave-node-config" assetpath="./"><template><style include="iron-flex ha-style">.content{margin-top:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}.help-text{padding-left:24px;padding-right:24px}</style><div class="content"><paper-card heading="Node config options"><template is="dom-if" if="[[wakeupNode]]"><div class="card-actions"><paper-input float-label="Wakeup Interval" type="number" value="{{wakeupInput}}" placeholder="[[computeGetWakeupValue(selectedNode)]]"><div suffix="">seconds</div></paper-input><ha-call-service-button hass="[[hass]]" domain="zwave" service="set_wakeup" service-data="[[computeWakeupServiceData(wakeupInput)]]">Set Wakeup</ha-call-service-button></div></template><div class="device-picker"><paper-dropdown-menu label="Config parameter" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedConfigParameter}}"><template is="dom-repeat" items="[[config]]" as="state"><paper-item>[[computeSelectCaptionConfigParameter(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'List')]]"><div class="device-picker"><paper-dropdown-menu label="Config value" class="flex" placeholder="{{loadedConfigValue}}"><paper-listbox class="dropdown-content" selected="{{selectedConfigValue}}"><template is="dom-repeat" items="[[selectedConfigParameterValues]]" as="state"><paper-item>[[state]]</paper-item></template></paper-listbox></paper-dropdown-menu></div></template><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'Byte Short Int')]]"><div class="card-actions"><paper-input label="{{selectedConfigParameterNumValues}}" type="number" value="{{selectedConfigValue}}" max="{{configParameterMax}}" min="{{configParameterMin}}"></paper-input></div></template><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'Bool Button')]]"><div class="device-picker"><paper-dropdown-menu label="Config value" class="flex" placeholder="{{loadedConfigValue}}"><paper-listbox class="dropdown-content" selected="{{selectedConfigValue}}"><template is="dom-repeat" items="[[selectedConfigParameterValues]]" as="state"><paper-item>[[state]]</paper-item></template></paper-listbox></paper-dropdown-menu></div></template><div class="help-text"><span>[[configValueHelpText]]</span></div><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'Bool Button Byte Short Int List')]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="set_config_parameter" service-data="[[computeSetConfigParameterServiceData(selectedConfigValue)]]">Set Config Parameter</ha-call-service-button></div></template></paper-card></div></template></dom-module><script>Polymer({
is: 'zwave-node-config',
@@ -313,131 +507,7 @@
this.selectedUserCodeChanged(this.selectedUserCode);
}.bind(this));
},
});</script><dom-module id="zwave-groups" assetpath="./"><template><style include="iron-flex ha-style">.content{margin-top:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}.help-text{padding-left:24px;padding-right:24px}</style><div class="content"><paper-card heading="Node group associations"><div class="device-picker"><paper-dropdown-menu label="Node to control" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedTargetNode}}"><template is="dom-repeat" items="[[nodes]]" as="state"><paper-item>[[computeSelectCaption(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsTargetNodeSelected(selectedTargetNode)]]"><div class="device-picker"><paper-dropdown-menu label="Group" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedGroup}}"><template is="dom-repeat" items="[[groups]]" as="state"><paper-item>[[computeSelectCaptionGroup(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div></template><template is="dom-if" if="[[!computeIsGroupSelected(selectedGroup)]]"><div class="help-text"><span>Other Nodes in this group:</span><template is="dom-repeat" items="[[otherGroupNodes]]" as="state"><span>[[state]]</span></template></div><div class="help-text"><span>Max Associations:</span> <span>[[maxAssociations]]</span></div><div class="card-actions"><template is="dom-if" if="[[!noAssociationsLeft]]"><ha-call-service-button hass="[[hass]]" domain="zwave" service="change_association" service-data="[[computeAssocServiceData(selectedGroup, &quot;add&quot;)]]">Add To Group</ha-call-service-button></template><ha-call-service-button hass="[[hass]]" domain="zwave" service="change_association" service-data="[[computeAssocServiceData(selectedGroup, &quot;remove&quot;)]]">Remove From Group</ha-call-service-button></div></template></paper-card></div></template></dom-module><script>Polymer({
is: 'zwave-groups',
properties: {
hass: {
type: Object,
},
nodes: {
type: Array,
},
groups: {
type: Array,
},
selectedNode: {
type: Number,
},
selectedTargetNode: {
type: Number,
value: -1
},
selectedGroup: {
type: Number,
value: -1,
observer: 'selectedGroupChanged'
},
otherGroupNodes: {
type: Array,
value: -1,
computed: 'computeOtherGroupNodes(selectedGroup)'
},
maxAssociations: {
type: String,
value: '',
computed: 'computeMaxAssociations(selectedGroup)'
},
noAssociationsLeft: {
type: Boolean,
value: true,
computed: 'computeAssociationsLeft(selectedGroup)'
},
},
listeners: {
'hass-service-called': 'serviceCalled',
},
serviceCalled: function (ev) {
if (ev.detail.success) {
var foo = this;
setTimeout(function () {
foo.refreshGroups(foo.selectedNode);
}, 5000);
}
},
computeAssociationsLeft: function (selectedGroup) {
if (selectedGroup === -1) return true;
return (this.maxAssociations === this.otherGroupNodes.length);
},
computeMaxAssociations: function (selectedGroup) {
if (selectedGroup === -1) return -1;
var maxAssociations = this.groups[selectedGroup].value.max_associations;
if (!maxAssociations) return ['None'];
return maxAssociations;
},
computeOtherGroupNodes: function (selectedGroup) {
if (selectedGroup === -1) return -1;
var associations = Object.values(this.groups[selectedGroup].value.associations);
if (!associations.length) return ['None'];
return associations;
},
computeSelectCaption: function (stateObj) {
return window.hassUtil.computeStateName(stateObj) + ' (Node:' +
stateObj.attributes.node_id + ' ' +
stateObj.attributes.query_stage + ')';
},
computeSelectCaptionGroup: function (stateObj) {
return (stateObj.key + ': ' + stateObj.value.label);
},
computeIsTargetNodeSelected: function (selectedTargetNode) {
return (!this.nodes || selectedTargetNode === -1);
},
computeIsGroupSelected: function (selectedGroup) {
return (!this.nodes || this.selectedNode === -1 || selectedGroup === -1);
},
computeAssocServiceData: function (selectedGroup, type) {
if (!this.groups === -1 || selectedGroup === -1 || this.selectedNode === -1) return -1;
return { node_id: this.nodes[this.selectedNode].attributes.node_id,
association: type,
target_node_id: this.nodes[this.selectedTargetNode].attributes.node_id,
group: this.groups[selectedGroup].key };
},
refreshGroups: function (selectedNode) {
var groupData = [];
this.hass.callApi('GET', 'zwave/groups/' + this.nodes[selectedNode].attributes.node_id).then(function (groups) {
Object.entries(groups).forEach(([key, value]) => {
groupData.push({ key, value });
});
this.groups = groupData;
this.selectedGroupChanged(this.selectedGroup);
}.bind(this));
},
selectedGroupChanged: function (selectedGroup) {
if (this.selectedGroup === -1 || selectedGroup === -1) return;
this.maxAssociations = this.groups[selectedGroup].value.max_associations;
this.otherGroupNodes = Object.values(this.groups[selectedGroup].value.associations);
},
});</script></div><dom-module id="ha-panel-zwave"><template><style include="iron-flex ha-style">.content{margin-top:24px}.node-info{margin-left:16px;text-transform:capitalize}.help-text{padding-left:24px;padding-right:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">Z-Wave Manager</div></app-toolbar></app-header><div class="content"><zwave-network id="zwave-network" hass="[[hass]]"></zwave-network></div><div class="content"><paper-card heading="Z-Wave Node Management"><div class="card-content">Z-Wave Node controls.</div><div class="device-picker"><paper-dropdown-menu label="Nodes" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedNode}}"><template is="dom-repeat" items="[[nodes]]" as="state"><paper-item>[[computeSelectCaption(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="refresh_node" service-data="[[computeNodeServiceData(selectedNode)]]">Refresh Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="remove_failed_node" service-data="[[computeNodeServiceData(selectedNode)]]">Remove Failed Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="replace_failed_node" service-data="[[computeNodeServiceData(selectedNode)]]">Replace Failed Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="print_node" service-data="[[computeNodeServiceData(selectedNode)]]">Print Node</ha-call-service-button></div><div class="card-actions"><paper-input float-label="New node name" type="text" value="{{newNodeNameInput}}" placeholder="[[computeGetNodeName(selectedNode)]]"></paper-input><ha-call-service-button hass="[[hass]]" domain="zwave" service="rename_node" service-data="[[computeNodeNameServiceData(newNodeNameInput)]]">Rename Node</ha-call-service-button></div><div class="device-picker"><paper-dropdown-menu label="Entities of this node" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedEntity}}"><template is="dom-repeat" items="[[entities]]" as="state"><paper-item>[[computeSelectCaptionEnt(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsEntitySelected(selectedEntity)]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="refresh_entity" service-data="[[computeRefreshEntityServiceData(selectedEntity)]]">Refresh Entity</ha-call-service-button></div><div class="content"><div class="card-actions"><paper-button toggles="" raised="" noink="" active="{{entityInfoActive}}">Entity Attributes</paper-button></div><template is="dom-if" if="{{entityInfoActive}}"><template is="dom-repeat" items="[[selectedEntityAttrs]]" as="state"><div class="node-info"><span>[[state]]</span></div></template></template></div></template></template></paper-card></div><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-node-information id="zwave-node-information" nodes="[[nodes]]" selected-node="[[selectedNode]]"></zwave-node-information></template><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-groups hass="[[hass]]" nodes="[[nodes]]" selected-node="[[selectedNode]]" groups="[[groups]]"></zwave-groups></template><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-node-config hass="[[hass]]" nodes="[[nodes]]" selected-node="[[selectedNode]]" config="[[config]]"></zwave-node-config></template><template is="dom-if" if="{{hasNodeUserCodes}}"><zwave-usercodes id="zwave-usercodes" hass="[[hass]]" nodes="[[nodes]]" user-codes="[[userCodes]]" selected-node="[[selectedNode]]"></zwave-usercodes></template><div class="content"><ozw-log id="ozw-log" hass="[[hass]]"></ozw-log></div></app-header-layout></template></dom-module><script>Polymer({
});</script></div><dom-module id="ha-panel-zwave"><template><style include="iron-flex ha-style">.content{margin-top:24px}.node-info{margin-left:16px;text-transform:capitalize}.help-text{padding-left:24px;padding-right:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">Z-Wave Manager</div></app-toolbar></app-header><div class="content"><zwave-network id="zwave-network" hass="[[hass]]"></zwave-network></div><div class="content"><paper-card heading="Z-Wave Node Management"><div class="card-content">Z-Wave Node controls.</div><div class="device-picker"><paper-dropdown-menu label="Nodes" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedNode}}"><template is="dom-repeat" items="[[nodes]]" as="state"><paper-item>[[computeSelectCaption(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="refresh_node" service-data="[[computeNodeServiceData(selectedNode)]]">Refresh Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="remove_failed_node" service-data="[[computeNodeServiceData(selectedNode)]]">Remove Failed Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="replace_failed_node" service-data="[[computeNodeServiceData(selectedNode)]]">Replace Failed Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="print_node" service-data="[[computeNodeServiceData(selectedNode)]]">Print Node</ha-call-service-button></div><div class="card-actions"><paper-input float-label="New node name" type="text" value="{{newNodeNameInput}}" placeholder="[[computeGetNodeName(selectedNode)]]"></paper-input><ha-call-service-button hass="[[hass]]" domain="zwave" service="rename_node" service-data="[[computeNodeNameServiceData(newNodeNameInput)]]">Rename Node</ha-call-service-button></div><div class="device-picker"><paper-dropdown-menu label="Entities of this node" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedEntity}}"><template is="dom-repeat" items="[[entities]]" as="state"><paper-item>[[computeSelectCaptionEnt(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsEntitySelected(selectedEntity)]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="refresh_entity" service-data="[[computeRefreshEntityServiceData(selectedEntity)]]">Refresh Entity</ha-call-service-button></div><div class="content"><div class="card-actions"><paper-button toggles="" raised="" noink="" active="{{entityInfoActive}}">Entity Attributes</paper-button></div><template is="dom-if" if="{{entityInfoActive}}"><template is="dom-repeat" items="[[selectedEntityAttrs]]" as="state"><div class="node-info"><span>[[state]]</span></div></template></template></div></template></template></paper-card></div><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-node-information id="zwave-node-information" nodes="[[nodes]]" selected-node="[[selectedNode]]"></zwave-node-information></template><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-values hass="[[hass]]" nodes="[[nodes]]" selected-node="[[selectedNode]]" values="[[values]]"></zwave-values></template><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-groups hass="[[hass]]" nodes="[[nodes]]" selected-node="[[selectedNode]]" groups="[[groups]]"></zwave-groups></template><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-node-config hass="[[hass]]" nodes="[[nodes]]" selected-node="[[selectedNode]]" config="[[config]]"></zwave-node-config></template><template is="dom-if" if="{{hasNodeUserCodes}}"><zwave-usercodes id="zwave-usercodes" hass="[[hass]]" nodes="[[nodes]]" user-codes="[[userCodes]]" selected-node="[[selectedNode]]"></zwave-usercodes></template><div class="content"><ozw-log id="ozw-log" hass="[[hass]]"></ozw-log></div></app-header-layout></template></dom-module><script>Polymer({
is: 'ha-panel-zwave',
properties: {
@@ -493,6 +563,10 @@
computed: 'computeSelectedEntityAttrs(selectedEntity)'
},
values: {
type: Array,
},
groups: {
type: Array,
},
@@ -559,6 +633,8 @@
},
selectedNodeChanged: function (selectedNode) {
this.newNodeNameInput = '';
if (selectedNode === -1) return;
this.selectedConfigParameter = -1;
this.selectedConfigParameterValue = -1;
@@ -570,6 +646,13 @@
});
this.config = configData;
}.bind(this));
var valueData = [];
this.hass.callApi('GET', 'zwave/values/' + this.nodes[selectedNode].attributes.node_id).then(function (values) {
Object.entries(values).forEach(([key, value]) => {
valueData.push({ key, value });
});
this.values = valueData;
}.bind(this));
var groupData = [];
this.hass.callApi('GET', 'zwave/groups/' + this.nodes[selectedNode].attributes.node_id).then(function (groups) {
Object.entries(groups).forEach(([key, value]) => {
@@ -630,9 +713,7 @@
computeGetNodeName: function (selectedNode) {
if (this.selectedNode === -1 ||
!this.nodes[selectedNode].entity_id) return -1;
var str = (this.nodes[selectedNode].entity_id);
var name = str.replace('zwave.', '');
return name;
return this.nodes[selectedNode].attributes.node_name;
},
computeNodeNameServiceData: function (newNodeNameInput) {
File diff suppressed because one or more lines are too long
+2 -2
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')
)
+5 -2
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()
+5 -5
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
+1 -1
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.26']
REQUIREMENTS = ['pyhomematic==0.1.27']
DOMAIN = 'homematic'
+2 -2
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):
+4 -5
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)
@@ -72,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
@@ -117,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):
+8 -2
View File
@@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/insteon_local/
"""
import logging
import os
import requests
import voluptuous as vol
@@ -13,7 +14,7 @@ from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, CONF_HOST, CONF_PORT, CONF_TIMEOUT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['insteonlocal==0.48']
REQUIREMENTS = ['insteonlocal==0.52']
_LOGGER = logging.getLogger(__name__)
@@ -47,7 +48,12 @@ def setup(hass, config):
timeout = conf.get(CONF_TIMEOUT)
try:
insteonhub = Hub(host, username, password, port, timeout, _LOGGER)
if not os.path.exists(hass.config.path('.insteon_cache')):
os.makedirs(hass.config.path('.insteon_cache'))
insteonhub = Hub(host, username, password, port, timeout, _LOGGER,
hass.config.path('.insteon_cache'))
# Check for successful connection
insteonhub.get_buffer_status()
except requests.exceptions.ConnectTimeout:
+1 -1
View File
@@ -167,7 +167,7 @@ IDENTIFY_SCHEMA = vol.Schema({
vol.Optional(ATTR_PUSH_SOUNDS): list
}, extra=vol.ALLOW_EXTRA)
CONFIGURATION_FILE = 'ios.conf'
CONFIGURATION_FILE = '.ios.conf'
CONFIG_FILE = {ATTR_DEVICES: {}}
+17 -12
View File
@@ -77,6 +77,8 @@ EFFECT_COLORLOOP = "colorloop"
EFFECT_RANDOM = "random"
EFFECT_WHITE = "white"
COLOR_GROUP = "Color descriptors"
LIGHT_PROFILES_FILE = "light_profiles.csv"
PROP_TO_ATTR = {
@@ -98,17 +100,21 @@ VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100))
LIGHT_TURN_ON_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
ATTR_PROFILE: cv.string,
vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string,
ATTR_TRANSITION: VALID_TRANSITION,
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
ATTR_COLOR_NAME: cv.string,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
vol.Coerce(tuple)),
ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)),
ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)),
vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string,
vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP):
vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP):
vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
vol.Coerce(tuple)),
vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Exclusive(ATTR_KELVIN, COLOR_GROUP):
vol.All(vol.Coerce(int), vol.Range(min=0)),
ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]),
ATTR_EFFECT: cv.string,
@@ -285,8 +291,8 @@ def async_setup(hass, config):
yield from asyncio.wait(update_tasks, loop=hass.loop)
# Listen for light on and light off 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'))
hass.services.async_register(
@@ -341,8 +347,7 @@ class Profiles:
return None
return profiles
cls._all = yield from hass.loop.run_in_executor(
None, load_profile_data, hass)
cls._all = yield from hass.async_add_job(load_profile_data, hass)
return cls._all is not None
@classmethod
+13 -35
View File
@@ -12,10 +12,9 @@ import voluptuous as vol
from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_EFFECT, ATTR_WHITE_VALUE,
EFFECT_COLORLOOP, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT,
SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE, Light,
PLATFORM_SCHEMA)
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_EFFECT, EFFECT_COLORLOOP,
EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT,
SUPPORT_RGB_COLOR, Light, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['flux_led==0.19']
@@ -27,10 +26,8 @@ ATTR_MODE = 'mode'
DOMAIN = 'flux_led'
SUPPORT_FLUX_LED_RGB = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT |
SUPPORT_RGB_COLOR)
SUPPORT_FLUX_LED_RGBW = (SUPPORT_WHITE_VALUE | SUPPORT_EFFECT |
SUPPORT_RGB_COLOR)
SUPPORT_FLUX_LED = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT |
SUPPORT_RGB_COLOR)
MODE_RGB = 'rgb'
MODE_RGBW = 'rgbw'
@@ -182,16 +179,7 @@ class FluxLight(Light):
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
if self._mode == MODE_RGB:
return self._bulb.brightness
return None # not used for RGBW
@property
def white_value(self):
"""Return the white value of this light between 0..255."""
if self._mode == MODE_RGBW:
return self._bulb.getRgbw()[3]
return None # not used for RGB
return self._bulb.brightness
@property
def rgb_color(self):
@@ -201,11 +189,7 @@ class FluxLight(Light):
@property
def supported_features(self):
"""Flag supported features."""
if self._mode == MODE_RGBW:
return SUPPORT_FLUX_LED_RGBW
elif self._mode == MODE_RGB:
return SUPPORT_FLUX_LED_RGB
return 0
return SUPPORT_FLUX_LED
@property
def effect_list(self):
@@ -219,23 +203,17 @@ class FluxLight(Light):
rgb = kwargs.get(ATTR_RGB_COLOR)
brightness = kwargs.get(ATTR_BRIGHTNESS)
white_value = kwargs.get(ATTR_WHITE_VALUE)
effect = kwargs.get(ATTR_EFFECT)
if rgb is not None and brightness is not None:
self._bulb.setRgb(*tuple(rgb), brightness=brightness)
elif rgb is not None and white_value is not None:
self._bulb.setRgbw(*tuple(rgb), w=white_value)
elif rgb is not None:
# self.white_value and self.brightness are appropriately
# returning None for MODE_RGB and MODE_RGBW respectively
self._bulb.setRgbw(*tuple(rgb),
w=self.white_value,
brightness=self.brightness)
self._bulb.setRgb(*tuple(rgb))
elif brightness is not None:
self._bulb.setRgb(*self.rgb_color, brightness=brightness)
elif white_value is not None:
self._bulb.setRgbw(*self.rgb_color, w=white_value)
if self._mode == 'rgbw':
self._bulb.setWarmWhite255(brightness)
elif self._mode == 'rgb':
(red, green, blue) = self._bulb.getRgb()
self._bulb.setRgb(red, green, blue, brightness=brightness)
elif effect == EFFECT_RANDOM:
self._bulb.setRgb(random.randint(0, 255),
random.randint(0, 255),
+17 -10
View File
@@ -37,7 +37,7 @@ from . import effects as lifx_effects
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['aiolifx==0.4.6']
REQUIREMENTS = ['aiolifx==0.4.7']
UDP_BROADCAST_PORT = 56700
@@ -54,9 +54,6 @@ ATTR_POWER = 'power'
BYTE_MAX = 255
SHORT_MAX = 65535
SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR |
SUPPORT_XY_COLOR | SUPPORT_TRANSITION | SUPPORT_EFFECT)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
})
@@ -229,6 +226,12 @@ class LIFXLight(Light):
self.set_power(device.power_level)
self.set_color(*device.color)
@property
def lifxwhite(self):
"""Return whether this is a white-only bulb."""
# https://lan.developer.lifx.com/docs/lifx-products
return self.product in [10, 11, 18]
@property
def available(self):
"""Return the availability of the device."""
@@ -273,8 +276,7 @@ class LIFXLight(Light):
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
# The 3 LIFX "White" products supported a limited temperature range
# https://lan.developer.lifx.com/docs/lifx-products
if self.product in [10, 11, 18]:
if self.lifxwhite:
kelvin = 6500
else:
kelvin = 9000
@@ -284,8 +286,7 @@ class LIFXLight(Light):
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
# The 3 LIFX "White" products supported a limited temperature range
# https://lan.developer.lifx.com/docs/lifx-products
if self.product in [10, 11, 18]:
if self.lifxwhite:
kelvin = 2700
else:
kelvin = 2500
@@ -305,12 +306,18 @@ class LIFXLight(Light):
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_LIFX
features = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP |
SUPPORT_TRANSITION | SUPPORT_EFFECT)
if not self.lifxwhite:
features |= SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR
return features
@property
def effect_list(self):
"""Return the list of supported effects."""
return lifx_effects.effect_list()
return lifx_effects.effect_list(self)
@asyncio.coroutine
def update_after_transition(self, now):
+24 -13
View File
@@ -7,7 +7,7 @@ import voluptuous as vol
from homeassistant.components.light import (
DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME,
ATTR_RGB_COLOR, ATTR_EFFECT, ATTR_TRANSITION,
ATTR_RGB_COLOR, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_EFFECT, ATTR_TRANSITION,
VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT)
from homeassistant.const import (ATTR_ENTITY_ID)
import homeassistant.helpers.config_validation as cv
@@ -42,6 +42,8 @@ LIFX_EFFECT_BREATHE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
ATTR_COLOR_NAME: cv.string,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)),
ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)),
vol.Optional(ATTR_PERIOD, default=1.0):
vol.All(vol.Coerce(float), vol.Range(min=0.05)),
vol.Optional(ATTR_CYCLES, default=1.0):
@@ -131,14 +133,21 @@ def default_effect(light, **kwargs):
yield from light.hass.services.async_call(DOMAIN, service, data)
def effect_list():
"""Return the list of supported effects."""
return [
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_BREATHE,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
def effect_list(light):
"""Return the list of supported effects for this light."""
if light.lifxwhite:
return [
SERVICE_EFFECT_BREATHE,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
else:
return [
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_BREATHE,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
class LIFXEffectData(object):
@@ -230,12 +239,14 @@ class LIFXEffectBreathe(LIFXEffect):
cycles = kwargs[ATTR_CYCLES]
hsbk, color_changed = light.find_hsbk(**kwargs)
# Default color is to fully (de)saturate with full brightness
# Set default effect color based on current setting
if not color_changed:
if hsbk[1] > 65536/2:
hsbk = [hsbk[0], 0, 65535, 4000]
if light.lifxwhite or hsbk[1] < 65536/2:
# White: toggle brightness
hsbk[2] = 65535 if hsbk[2] < 65536/2 else 0
else:
hsbk = [hsbk[0], 65535, 65535, hsbk[3]]
# Color: fully desaturate with full brightness
hsbk = [hsbk[0], 0, 65535, 4000]
# Start the effect
args = {
+236
View File
@@ -0,0 +1,236 @@
"""
Support for Template lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.template/
"""
import logging
import asyncio
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, Light, SUPPORT_BRIGHTNESS)
from homeassistant.const import (
CONF_VALUE_TEMPLATE, CONF_ENTITY_ID, CONF_FRIENDLY_NAME, STATE_ON,
STATE_OFF, EVENT_HOMEASSISTANT_START, MATCH_ALL
)
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.helpers.script import Script
_LOGGER = logging.getLogger(__name__)
_VALID_STATES = [STATE_ON, STATE_OFF, 'true', 'false']
CONF_LIGHTS = 'lights'
CONF_ON_ACTION = 'turn_on'
CONF_OFF_ACTION = 'turn_off'
CONF_LEVEL_ACTION = 'set_level'
CONF_LEVEL_TEMPLATE = 'level_template'
LIGHT_SCHEMA = vol.Schema({
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_VALUE_TEMPLATE, default=None): cv.template,
vol.Optional(CONF_LEVEL_ACTION, default=None): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_LEVEL_TEMPLATE, default=None): cv.template,
vol.Optional(CONF_FRIENDLY_NAME, default=None): cv.string,
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_LIGHTS): vol.Schema({cv.slug: LIGHT_SCHEMA}),
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up Template Lights."""
lights = []
for device, device_config in config[CONF_LIGHTS].items():
friendly_name = device_config.get(CONF_FRIENDLY_NAME, device)
state_template = device_config[CONF_VALUE_TEMPLATE]
on_action = device_config[CONF_ON_ACTION]
off_action = device_config[CONF_OFF_ACTION]
level_action = device_config[CONF_LEVEL_ACTION]
level_template = device_config[CONF_LEVEL_TEMPLATE]
template_entity_ids = set()
if state_template is not None:
temp_ids = state_template.extract_entities()
if str(temp_ids) != MATCH_ALL:
template_entity_ids |= set(temp_ids)
if level_template is not None:
temp_ids = level_template.extract_entities()
if str(temp_ids) != MATCH_ALL:
template_entity_ids |= set(temp_ids)
if not template_entity_ids:
template_entity_ids = MATCH_ALL
entity_ids = device_config.get(CONF_ENTITY_ID, template_entity_ids)
lights.append(
LightTemplate(
hass, device, friendly_name, state_template,
on_action, off_action, level_action, level_template,
entity_ids)
)
if not lights:
_LOGGER.error("No lights added")
return False
async_add_devices(lights, True)
return True
class LightTemplate(Light):
"""Representation of a templated Light, including dimmable."""
def __init__(self, hass, device_id, friendly_name, state_template,
on_action, off_action, level_action, level_template,
entity_ids):
"""Initialize the light."""
self.hass = hass
self.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, device_id, hass=hass)
self._name = friendly_name
self._template = state_template
self._on_script = Script(hass, on_action)
self._off_script = Script(hass, off_action)
self._level_script = Script(hass, level_action)
self._level_template = level_template
self._state = False
self._brightness = None
self._entities = entity_ids
if self._template is not None:
self._template.hass = self.hass
if self._level_template is not None:
self._level_template.hass = self.hass
@property
def brightness(self):
"""Return the brightness of the light."""
return self._brightness
@property
def supported_features(self):
"""Flag supported features."""
if self._level_script is not None:
return SUPPORT_BRIGHTNESS
return 0
@property
def is_on(self):
"""Return true if device is on."""
return self._state
@property
def should_poll(self):
"""Return the polling state."""
return False
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
state = yield from async_get_last_state(self.hass, self.entity_id)
if state:
self._state = state.state == STATE_ON
@callback
def template_light_state_listener(entity, old_state, new_state):
"""Handle target device state changes."""
self.hass.async_add_job(self.async_update_ha_state(True))
@callback
def template_light_startup(event):
"""Update template on startup."""
if (self._template is not None or
self._level_template is not None):
async_track_state_change(
self.hass, self._entities, template_light_state_listener)
self.hass.async_add_job(self.async_update_ha_state(True))
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, template_light_startup)
@asyncio.coroutine
def async_turn_on(self, **kwargs):
"""Turn the light on."""
optimistic_set = False
# set optimistic states
if self._template is None:
self._state = True
optimistic_set = True
if self._level_template is None and ATTR_BRIGHTNESS in kwargs:
_LOGGER.info("Optimistically setting brightness to %s",
kwargs[ATTR_BRIGHTNESS])
self._brightness = kwargs[ATTR_BRIGHTNESS]
optimistic_set = True
if ATTR_BRIGHTNESS in kwargs and self._level_script:
self.hass.async_add_job(self._level_script.async_run(
{"brightness": kwargs[ATTR_BRIGHTNESS]}))
else:
self.hass.async_add_job(self._on_script.async_run())
if optimistic_set:
self.hass.async_add_job(self.async_update_ha_state())
@asyncio.coroutine
def async_turn_off(self, **kwargs):
"""Turn the light off."""
self.hass.async_add_job(self._off_script.async_run())
if self._template is None:
self._state = False
self.hass.async_add_job(self.async_update_ha_state())
@asyncio.coroutine
def async_update(self):
"""Update the state from the template."""
if self._template is not None:
try:
state = self._template.async_render().lower()
except TemplateError as ex:
_LOGGER.error(ex)
self._state = None
if state in _VALID_STATES:
self._state = state in ('true', STATE_ON)
else:
_LOGGER.error(
'Received invalid light is_on state: %s. ' +
'Expected: %s',
state, ', '.join(_VALID_STATES))
self._state = None
if self._level_template is not None:
try:
brightness = self._level_template.async_render()
except TemplateError as ex:
_LOGGER.error(ex)
self._state = None
if 0 <= int(brightness) <= 255:
self._brightness = brightness
else:
_LOGGER.error(
'Received invalid brightness : %s' +
'Expected: 0-255',
brightness)
self._brightness = None
-4
View File
@@ -19,8 +19,6 @@ DEPENDENCIES = ['wink']
SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR
RGB_MODES = ['hsb', 'rgb']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Wink lights."""
@@ -62,8 +60,6 @@ class WinkLight(WinkDevice, Light):
"""Define current bulb color in RGB."""
if not self.wink.supports_hue_saturation():
return None
elif self.wink.color_model() not in RGB_MODES:
return False
else:
hue = self.wink.color_hue()
saturation = self.wink.color_saturation()
+4 -6
View File
@@ -108,8 +108,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'))
hass.services.async_register(
@@ -150,8 +150,7 @@ class LockDevice(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.lock, **kwargs))
return self.hass.async_add_job(ft.partial(self.lock, **kwargs))
def unlock(self, **kwargs):
"""Unlock the lock."""
@@ -162,8 +161,7 @@ class LockDevice(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.unlock, **kwargs))
return self.hass.async_add_job(ft.partial(self.unlock, **kwargs))
@property
def state_attributes(self):
+1 -1
View File
@@ -128,7 +128,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
network = hass.data[zwave.ZWAVE_NETWORK]
network = hass.data[zwave.const.DATA_NETWORK]
def set_usercode(service):
"""Set the usercode to index X on the lock."""
+2 -2
View File
@@ -134,8 +134,8 @@ class LogbookView(HomeAssistantView):
end_day = start_day + timedelta(days=1)
hass = request.app['hass']
events = yield from hass.loop.run_in_executor(
None, _get_events, hass, start_day, end_day)
events = yield from hass.async_add_job(
_get_events, hass, start_day, end_day)
events = _exclude_events(events, self.config)
return self.json(humanify(events))
+2 -2
View File
@@ -123,8 +123,8 @@ def async_setup(hass, config):
"""Handle logger services."""
set_log_levels(service.data)
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(
+1 -2
View File
@@ -10,8 +10,7 @@ import logging
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['https://github.com/thecynic/pylutron/archive/v0.1.0.zip#'
'pylutron==0.1.0']
REQUIREMENTS = ['pylutron==0.1.0']
DOMAIN = 'lutron'
@@ -353,8 +353,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, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine
@@ -583,8 +583,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.turn_on)
return self.hass.async_add_job(self.turn_on)
def turn_off(self):
"""Turn the media player off."""
@@ -595,8 +594,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.turn_off)
return self.hass.async_add_job(self.turn_off)
def mute_volume(self, mute):
"""Mute the volume."""
@@ -607,8 +605,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.mute_volume, mute)
return self.hass.async_add_job(self.mute_volume, mute)
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
@@ -619,8 +616,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.set_volume_level, volume)
return self.hass.async_add_job(self.set_volume_level, volume)
def media_play(self):
"""Send play commmand."""
@@ -631,8 +627,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.media_play)
return self.hass.async_add_job(self.media_play)
def media_pause(self):
"""Send pause command."""
@@ -643,8 +638,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.media_pause)
return self.hass.async_add_job(self.media_pause)
def media_stop(self):
"""Send stop command."""
@@ -655,8 +649,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.media_stop)
return self.hass.async_add_job(self.media_stop)
def media_previous_track(self):
"""Send previous track command."""
@@ -667,8 +660,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.media_previous_track)
return self.hass.async_add_job(self.media_previous_track)
def media_next_track(self):
"""Send next track command."""
@@ -679,8 +671,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.media_next_track)
return self.hass.async_add_job(self.media_next_track)
def media_seek(self, position):
"""Send seek command."""
@@ -691,8 +682,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.media_seek, position)
return self.hass.async_add_job(self.media_seek, position)
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
@@ -703,8 +693,8 @@ class MediaPlayerDevice(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.play_media, media_type, media_id, **kwargs))
return self.hass.async_add_job(
ft.partial(self.play_media, media_type, media_id, **kwargs))
def select_source(self, source):
"""Select input source."""
@@ -715,8 +705,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.select_source, source)
return self.hass.async_add_job(self.select_source, source)
def clear_playlist(self):
"""Clear players playlist."""
@@ -727,8 +716,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.clear_playlist)
return self.hass.async_add_job(self.clear_playlist)
def set_shuffle(self, shuffle):
"""Enable/disable shuffle mode."""
@@ -739,8 +727,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.set_shuffle, shuffle)
return self.hass.async_add_job(self.set_shuffle, shuffle)
# No need to overwrite these.
@property
@@ -810,7 +797,7 @@ class MediaPlayerDevice(Entity):
"""
if hasattr(self, 'toggle'):
# pylint: disable=no-member
return self.hass.loop.run_in_executor(None, self.toggle)
return self.hass.async_add_job(self.toggle)
if self.state in [STATE_OFF, STATE_IDLE]:
return self.async_turn_on()
@@ -825,7 +812,7 @@ class MediaPlayerDevice(Entity):
"""
if hasattr(self, 'volume_up'):
# pylint: disable=no-member
yield from self.hass.loop.run_in_executor(None, self.volume_up)
yield from self.hass.async_add_job(self.volume_up)
return
if self.volume_level < 1:
@@ -840,7 +827,7 @@ class MediaPlayerDevice(Entity):
"""
if hasattr(self, 'volume_down'):
# pylint: disable=no-member
yield from self.hass.loop.run_in_executor(None, self.volume_down)
yield from self.hass.async_add_job(self.volume_down)
return
if self.volume_level > 0:
@@ -854,7 +841,7 @@ class MediaPlayerDevice(Entity):
"""
if hasattr(self, 'media_play_pause'):
# pylint: disable=no-member
return self.hass.loop.run_in_executor(None, self.media_play_pause)
return self.hass.async_add_job(self.media_play_pause)
if self.state == STATE_PLAYING:
return self.async_media_pause()
@@ -960,7 +947,7 @@ class MediaPlayerImageView(HomeAssistantView):
return web.Response(status=status)
authenticated = (request[KEY_AUTHENTICATED] or
request.GET.get('token') == player.access_token)
request.query.get('token') == player.access_token)
if not authenticated:
return web.Response(status=401)
@@ -19,7 +19,7 @@ from homeassistant.const import (
CONF_NAME, STATE_ON)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['denonavr==0.4.1']
REQUIREMENTS = ['denonavr==0.4.2']
_LOGGER = logging.getLogger(__name__)
@@ -175,8 +175,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if hass.services.has_service(DOMAIN, SERVICE_ADD_MEDIA):
return
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:
@@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF)
REQUIREMENTS = ['openhomedevice==0.2.1']
REQUIREMENTS = ['openhomedevice==0.4.2']
SUPPORT_OPENHOME = SUPPORT_SELECT_SOURCE | \
SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \
@@ -60,7 +60,6 @@ class OpenhomeDevice(MediaPlayerDevice):
self._track_information = {}
self._in_standby = None
self._transport_state = None
self._track_information = None
self._volume_level = None
self._volume_muted = None
self._supported_features = SUPPORT_OPENHOME
@@ -92,7 +91,7 @@ class OpenhomeDevice(MediaPlayerDevice):
if self._source["type"] == "Radio":
self._supported_features |= SUPPORT_STOP | SUPPORT_PLAY
if self._source["type"] == "Playlist":
if self._source["type"] in ("Playlist", "Cloud"):
self._supported_features |= SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PLAY
@@ -173,27 +172,29 @@ class OpenhomeDevice(MediaPlayerDevice):
@property
def media_image_url(self):
"""Image url of current playing media."""
return self._track_information["albumArt"]
return self._track_information.get('albumArtwork')
@property
def media_artist(self):
"""Artist of current playing media, music track only."""
return self._track_information["artist"]
artists = self._track_information.get('artist')
if artists:
return artists[0]
@property
def media_album_name(self):
"""Album name of current playing media, music track only."""
return self._track_information["album"]
return self._track_information.get('albumTitle')
@property
def media_title(self):
"""Title of current playing media."""
return self._track_information["title"]
return self._track_information.get('title')
@property
def source(self):
"""Name of the current input source."""
return self._source["name"]
return self._source.get('name')
@property
def volume_level(self):
@@ -125,8 +125,7 @@ class RokuDevice(MediaPlayerDevice):
"""Return the name of the device."""
if self.device_info.userdevicename:
return self.device_info.userdevicename
else:
return "roku_" + self.roku.device_info.sernum
return "Roku {}".format(self.device_info.sernum)
@property
def state(self):
@@ -158,8 +157,7 @@ class RokuDevice(MediaPlayerDevice):
return None
elif self.current_app.name == "Roku":
return None
else:
return MEDIA_TYPE_VIDEO
return MEDIA_TYPE_VIDEO
@property
def media_image_url(self):
@@ -165,6 +165,22 @@ shuffle_set:
description: True/false for enabling/disabling shuffle
example: true
snapcast_snapshot:
description: Take a snapshot of the media player.
fields:
entity_id:
description: Name(s) of entites that will be snapshotted. Platform dependent.
example: 'media_player.living_room'
snapcast_restore:
description: Restore a snapshot of the media player.
fields:
entity_id:
description: Name(s) of entites that will be restored. Platform dependent.
example: 'media_player.living_room'
sonos_join:
description: Group player together.
+181 -40
View File
@@ -4,62 +4,197 @@ Support for interacting with Snapcast clients.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.snapcast/
"""
import asyncio
import logging
from os import path
import socket
import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE,
SUPPORT_PLAY, PLATFORM_SCHEMA, MediaPlayerDevice)
PLATFORM_SCHEMA, MediaPlayerDevice)
from homeassistant.const import (
STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, CONF_HOST, CONF_PORT)
STATE_ON, STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, CONF_HOST,
CONF_PORT, ATTR_ENTITY_ID)
import homeassistant.helpers.config_validation as cv
from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['snapcast==1.2.2']
REQUIREMENTS = ['snapcast==2.0.6']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'snapcast'
SUPPORT_SNAPCAST = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY
SERVICE_SNAPSHOT = 'snapcast_snapshot'
SERVICE_RESTORE = 'snapcast_restore'
SUPPORT_SNAPCAST_CLIENT = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET
SUPPORT_SNAPCAST_GROUP = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET |\
SUPPORT_SELECT_SOURCE
GROUP_PREFIX = 'snapcast_group_'
GROUP_SUFFIX = 'Snapcast Group'
CLIENT_PREFIX = 'snapcast_client_'
CLIENT_SUFFIX = 'Snapcast Client'
SERVICE_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_PORT): cv.port
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Snapcast platform."""
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup the Snapcast platform."""
import snapcast.control
from snapcast.control.server import CONTROL_PORT
host = config.get(CONF_HOST)
port = config.get(CONF_PORT, snapcast.control.CONTROL_PORT)
port = config.get(CONF_PORT, CONTROL_PORT)
@asyncio.coroutine
def _handle_service(service):
"""Handle services."""
entity_ids = service.data.get(ATTR_ENTITY_ID)
devices = [device for device in hass.data[DOMAIN]
if device.entity_id in entity_ids]
for device in devices:
if service.service == SERVICE_SNAPSHOT:
device.snapshot()
elif service.service == SERVICE_RESTORE:
yield from device.async_restore()
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.async_register(
DOMAIN, SERVICE_SNAPSHOT, _handle_service,
descriptions.get(SERVICE_SNAPSHOT), schema=SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_RESTORE, _handle_service,
descriptions.get(SERVICE_RESTORE), schema=SERVICE_SCHEMA)
try:
server = snapcast.control.Snapserver(host, port)
server = yield from snapcast.control.create_server(
hass.loop, host, port)
except socket.gaierror:
_LOGGER.error(
"Could not connect to Snapcast server at %s:%d", host, port)
_LOGGER.error('Could not connect to Snapcast server at %s:%d',
host, port)
return False
groups = [SnapcastGroupDevice(group) for group in server.groups]
clients = [SnapcastClientDevice(client) for client in server.clients]
devices = groups + clients
hass.data[DOMAIN] = devices
async_add_devices(devices)
return True
class SnapcastGroupDevice(MediaPlayerDevice):
"""Representation of a Snapcast group device."""
def __init__(self, group):
"""Initialize the Snapcast group device."""
group.set_callback(self.schedule_update_ha_state)
self._group = group
@property
def state(self):
"""Return the state of the player."""
return {
'idle': STATE_IDLE,
'playing': STATE_PLAYING,
'unknown': STATE_UNKNOWN,
}.get(self._group.stream_status, STATE_UNKNOWN)
@property
def name(self):
"""Return the name of the device."""
return '{}{}'.format(GROUP_PREFIX, self._group.identifier)
@property
def source(self):
"""Return the current input source."""
return self._group.stream
@property
def volume_level(self):
"""Return the volume level."""
return self._group.volume / 100
@property
def is_volume_muted(self):
"""Volume muted."""
return self._group.muted
@property
def supported_features(self):
"""Flag media player features that are supported."""
return SUPPORT_SNAPCAST_GROUP
@property
def source_list(self):
"""List of available input sources."""
return list(self._group.streams_by_name().keys())
@property
def device_state_attributes(self):
"""Return the state attributes."""
name = '{} {}'.format(self._group.friendly_name, GROUP_SUFFIX)
return {
'friendly_name': name
}
@property
def should_poll(self):
"""Do not poll for state."""
return False
add_devices([SnapcastDevice(client) for client in server.clients])
@asyncio.coroutine
def async_select_source(self, source):
"""Set input source."""
streams = self._group.streams_by_name()
if source in streams:
yield from self._group.set_stream(streams[source].identifier)
self.hass.async_add_job(self.async_update_ha_state())
@asyncio.coroutine
def async_mute_volume(self, mute):
"""Send the mute command."""
yield from self._group.set_muted(mute)
self.hass.async_add_job(self.async_update_ha_state())
@asyncio.coroutine
def async_set_volume_level(self, volume):
"""Set the volume level."""
yield from self._group.set_volume(round(volume * 100))
self.hass.async_add_job(self.async_update_ha_state())
def snapshot(self):
"""Snapshot the group state."""
self._group.snapshot()
@asyncio.coroutine
def async_restore(self):
"""Restore the group state."""
yield from self._group.restore()
class SnapcastDevice(MediaPlayerDevice):
class SnapcastClientDevice(MediaPlayerDevice):
"""Representation of a Snapcast client device."""
def __init__(self, client):
"""Initialize the Snapcast device."""
"""Initialize the Snapcast client device."""
client.set_callback(self.schedule_update_ha_state)
self._client = client
@property
def name(self):
"""Return the name of the device."""
return self._client.identifier
return '{}{}'.format(CLIENT_PREFIX, self._client.identifier)
@property
def volume_level(self):
@@ -74,39 +209,45 @@ class SnapcastDevice(MediaPlayerDevice):
@property
def supported_features(self):
"""Flag media player features that are supported."""
return SUPPORT_SNAPCAST
return SUPPORT_SNAPCAST_CLIENT
@property
def state(self):
"""Return the state of the player."""
if not self._client.connected:
return STATE_OFF
if self._client.connected:
return STATE_ON
return STATE_OFF
@property
def device_state_attributes(self):
"""Return the state attributes."""
name = '{} {}'.format(self._client.friendly_name, CLIENT_SUFFIX)
return {
'idle': STATE_IDLE,
'playing': STATE_PLAYING,
'unknown': STATE_UNKNOWN,
}.get(self._client.stream.status, STATE_UNKNOWN)
'friendly_name': name
}
@property
def source(self):
"""Return the current input source."""
return self._client.stream.name
def should_poll(self):
"""Do not poll for state."""
return False
@property
def source_list(self):
"""List of available input sources."""
return list(self._client.streams_by_name().keys())
def mute_volume(self, mute):
@asyncio.coroutine
def async_mute_volume(self, mute):
"""Send the mute command."""
self._client.muted = mute
yield from self._client.set_muted(mute)
self.hass.async_add_job(self.async_update_ha_state())
def set_volume_level(self, volume):
@asyncio.coroutine
def async_set_volume_level(self, volume):
"""Set the volume level."""
self._client.volume = round(volume * 100)
yield from self._client.set_volume(round(volume * 100))
self.hass.async_add_job(self.async_update_ha_state())
def select_source(self, source):
"""Set input source."""
streams = self._client.streams_by_name()
if source in streams:
self._client.stream = streams[source].identifier
def snapshot(self):
"""Snapshot the client state."""
self._client.snapshot()
@asyncio.coroutine
def async_restore(self):
"""Restore the client state."""
yield from self._client.restore()
+48 -5
View File
@@ -37,6 +37,7 @@ _LOGGER = logging.getLogger(__name__)
# speaker every 10 seconds. Quiet it down a bit to just actual problems.
_SOCO_LOGGER = logging.getLogger('soco')
_SOCO_LOGGER.setLevel(logging.ERROR)
_SOCO_SERVICES_LOGGER = logging.getLogger('soco.services')
_REQUESTS_LOGGER = logging.getLogger('requests')
_REQUESTS_LOGGER.setLevel(logging.ERROR)
@@ -73,6 +74,8 @@ ATTR_WITH_GROUP = 'with_group'
ATTR_IS_COORDINATOR = 'is_coordinator'
UPNP_ERRORS_TO_IGNORE = ['701']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_ADVERTISE_ADDR): cv.string,
vol.Optional(CONF_INTERFACE_ADDR): cv.string,
@@ -255,10 +258,36 @@ def soco_error(funct):
return funct(*args, **kwargs)
except SoCoException as err:
_LOGGER.error("Error on %s with %s", funct.__name__, err)
return wrapper
def soco_filter_upnperror(errorcodes=None):
"""Filter out specified UPnP errors from logs."""
def decorator(funct):
"""Decorator function."""
@ft.wraps(funct)
def wrapper(*args, **kwargs):
"""Wrap for all soco UPnP exception."""
from soco.exceptions import SoCoUPnPException
# Temporarily disable SoCo logging because it will log the
# UPnP exception otherwise
_SOCO_SERVICES_LOGGER.disabled = True
try:
return funct(*args, **kwargs)
except SoCoUPnPException as err:
if err.error_code in errorcodes:
pass
else:
raise
finally:
_SOCO_SERVICES_LOGGER.disabled = False
return wrapper
return decorator
def soco_coordinator(funct):
"""Call function on coordinator."""
@ft.wraps(funct)
@@ -297,6 +326,7 @@ class SonosDevice(MediaPlayerDevice):
self._media_next_title = None
self._support_previous_track = False
self._support_next_track = False
self._support_play = False
self._support_stop = False
self._support_pause = False
self._current_track_uri = None
@@ -411,6 +441,7 @@ class SonosDevice(MediaPlayerDevice):
self._current_track_is_radio_stream = False
self._support_previous_track = False
self._support_next_track = False
self._support_play = False
self._support_stop = False
self._support_pause = False
self._is_playing_tv = False
@@ -494,6 +525,7 @@ class SonosDevice(MediaPlayerDevice):
support_previous_track = False
support_next_track = False
support_play = False
support_stop = False
support_pause = False
@@ -515,7 +547,8 @@ class SonosDevice(MediaPlayerDevice):
)
support_previous_track = False
support_next_track = False
support_stop = False
support_play = True
support_stop = True
support_pause = False
source_name = 'Radio'
@@ -578,6 +611,7 @@ class SonosDevice(MediaPlayerDevice):
)
support_previous_track = True
support_next_track = True
support_play = True
support_stop = True
support_pause = True
@@ -631,7 +665,7 @@ class SonosDevice(MediaPlayerDevice):
if playlist_position is not None and playlist_size is not None:
if playlist_position == 1:
if playlist_position <= 1:
support_previous_track = False
if playlist_position == playlist_size:
@@ -651,6 +685,7 @@ class SonosDevice(MediaPlayerDevice):
self._current_track_is_radio_stream = is_radio_stream
self._support_previous_track = support_previous_track
self._support_next_track = support_next_track
self._support_play = support_play
self._support_stop = support_stop
self._support_pause = support_pause
self._is_playing_tv = is_playing_tv
@@ -813,6 +848,9 @@ class SonosDevice(MediaPlayerDevice):
if not self._support_next_track:
supported = supported ^ SUPPORT_NEXT_TRACK
if not self._support_play:
supported = supported ^ SUPPORT_PLAY
if not self._support_stop:
supported = supported ^ SUPPORT_STOP
@@ -889,21 +927,25 @@ class SonosDevice(MediaPlayerDevice):
@soco_error
def turn_off(self):
"""Turn off media player."""
self.media_pause()
if self._support_pause:
self.media_pause()
@soco_error
@soco_filter_upnperror(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
def media_play(self):
"""Send play command."""
self._player.play()
@soco_error
@soco_filter_upnperror(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
def media_stop(self):
"""Send stop command."""
self._player.stop()
@soco_error
@soco_filter_upnperror(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
def media_pause(self):
"""Send pause command."""
@@ -936,7 +978,8 @@ class SonosDevice(MediaPlayerDevice):
@soco_error
def turn_on(self):
"""Turn the media player on."""
self.media_play()
if self.support_play:
self.media_play()
@soco_error
@soco_coordinator
@@ -40,6 +40,7 @@ AUTH_CALLBACK_NAME = 'api:spotify'
ICON = 'mdi:spotify'
DEFAULT_NAME = 'Spotify'
DOMAIN = 'spotify'
CONF_ALIASES = 'aliases'
CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'
CONF_CACHE_PATH = 'cache_path'
@@ -52,7 +53,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_CACHE_PATH): cv.string
vol.Optional(CONF_CACHE_PATH): cv.string,
vol.Optional(CONF_ALIASES, default={}): {cv.string: cv.string}
})
SCAN_INTERVAL = timedelta(seconds=30)
@@ -89,7 +91,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
configurator = get_component('configurator')
configurator.request_done(hass.data.get(DOMAIN))
del hass.data[DOMAIN]
player = SpotifyMediaPlayer(oauth, config.get(CONF_NAME, DEFAULT_NAME))
player = SpotifyMediaPlayer(oauth, config.get(CONF_NAME, DEFAULT_NAME),
config[CONF_ALIASES])
add_devices([player], True)
@@ -110,14 +113,14 @@ class SpotifyAuthCallbackView(HomeAssistantView):
def get(self, request):
"""Receive authorization token."""
hass = request.app['hass']
self.oauth.get_access_token(request.GET['code'])
self.oauth.get_access_token(request.query['code'])
hass.async_add_job(setup_platform, hass, self.config, self.add_devices)
class SpotifyMediaPlayer(MediaPlayerDevice):
"""Representation of a Spotify controller."""
def __init__(self, oauth, name):
def __init__(self, oauth, name, aliases):
"""Initialize."""
self._name = name
self._oauth = oauth
@@ -128,10 +131,11 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
self._image_url = None
self._state = STATE_UNKNOWN
self._current_device = None
self._devices = None
self._devices = {}
self._volume = None
self._shuffle = False
self._player = None
self._aliases = aliases
self._token_info = self._oauth.get_cached_token()
def refresh_spotify_instance(self):
@@ -154,10 +158,19 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
"""Update state and attributes."""
self.refresh_spotify_instance()
# Available devices
devices = self._player.devices().get('devices')
player_devices = self._player.devices()
if player_devices is not None:
devices = player_devices.get('devices')
if devices is not None:
self._devices = {device.get('name'): device.get('id')
old_devices = self._devices
self._devices = {self._aliases.get(device.get('id'),
device.get('name')):
device.get('id')
for device in devices}
device_diff = {name: id for name, id in self._devices.items()
if old_devices.get(name, None) is None}
if len(device_diff) > 0:
_LOGGER.info("New Devices: %s", str(device_diff))
# Current playback state
current = self._player.current_playback()
if current is None:
@@ -212,8 +225,9 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
def select_source(self, source):
"""Select playback device."""
self._player.transfer_playback(self._devices[source],
self._state == STATE_PLAYING)
if self._devices:
self._player.transfer_playback(self._devices[source],
self._state == STATE_PLAYING)
def play_media(self, media_type, media_id, **kwargs):
"""Play media."""
@@ -258,7 +272,8 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
@property
def source_list(self):
"""Return a list of source devices."""
return list(self._devices.keys())
if self._devices:
return list(self._devices.keys())
@property
def source(self):
@@ -190,6 +190,8 @@ class Volumio(MediaPlayerDevice):
def async_media_pause(self):
"""Send media_pause command to media player."""
if self._state['trackType'] == 'webradio':
return self.send_volumio_msg('commands', params={'cmd': 'stop'})
return self.send_volumio_msg('commands', params={'cmd': 'pause'})
def async_set_volume_level(self, volume):
+2 -2
View File
@@ -133,8 +133,8 @@ def async_setup(hass, config):
hass.data[DATA_MICROSOFT_FACE] = face
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
+1 -2
View File
@@ -16,8 +16,7 @@ from homeassistant.const import (
DOMAIN = 'modbus'
REQUIREMENTS = ['https://github.com/bashwork/pymodbus/archive/'
'd7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0']
REQUIREMENTS = ['pymodbus==1.3.0rc1']
# Type of network
CONF_BAUDRATE = 'baudrate'
+11 -11
View File
@@ -403,8 +403,8 @@ def async_setup(hass, config):
yield from hass.data[DATA_MQTT].async_publish(
msg_topic, payload, qos, retain)
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(
@@ -477,8 +477,8 @@ class MQTT(object):
This method must be run in the event loop and returns a coroutine.
"""
with (yield from self._paho_lock):
yield from self.hass.loop.run_in_executor(
None, self._mqttc.publish, topic, payload, qos, retain)
yield from self.hass.async_add_job(
self._mqttc.publish, topic, payload, qos, retain)
@asyncio.coroutine
def async_connect(self):
@@ -486,8 +486,8 @@ class MQTT(object):
This method is a coroutine.
"""
result = yield from self.hass.loop.run_in_executor(
None, self._mqttc.connect, self.broker, self.port, self.keepalive)
result = yield from self.hass.async_add_job(
self._mqttc.connect, self.broker, self.port, self.keepalive)
if result != 0:
import paho.mqtt.client as mqtt
@@ -507,7 +507,7 @@ class MQTT(object):
self._mqttc.disconnect()
self._mqttc.loop_stop()
return self.hass.loop.run_in_executor(None, stop)
return self.hass.async_add_job(stop)
@asyncio.coroutine
def async_subscribe(self, topic, qos):
@@ -522,8 +522,8 @@ class MQTT(object):
if topic in self.topics:
return
result, mid = yield from self.hass.loop.run_in_executor(
None, self._mqttc.subscribe, topic, qos)
result, mid = yield from self.hass.async_add_job(
self._mqttc.subscribe, topic, qos)
_raise_on_error(result)
self.progress[mid] = topic
@@ -535,8 +535,8 @@ class MQTT(object):
This method is a coroutine.
"""
result, mid = yield from self.hass.loop.run_in_executor(
None, self._mqttc.unsubscribe, topic)
result, mid = yield from self.hass.async_add_job(
self._mqttc.unsubscribe, topic)
_raise_on_error(result)
self.progress[mid] = topic
+17 -1
View File
@@ -17,7 +17,8 @@ from homeassistant.components.mqtt import CONF_STATE_TOPIC
_LOGGER = logging.getLogger(__name__)
TOPIC_MATCHER = re.compile(
r'(?P<prefix_topic>\w+)/(?P<component>\w+)/(?P<object_id>\w+)/config')
r'(?P<prefix_topic>\w+)/(?P<component>\w+)/(?P<object_id>[a-zA-Z0-9_-]+)'
'/config')
SUPPORTED_COMPONENTS = ['binary_sensor', 'light', 'sensor', 'switch']
@@ -28,6 +29,8 @@ ALLOWED_PLATFORMS = {
'switch': ['mqtt'],
}
ALREADY_DISCOVERED = 'mqtt_discovered_components'
@asyncio.coroutine
def async_start(hass, discovery_topic, hass_config):
@@ -65,6 +68,19 @@ def async_start(hass, discovery_topic, hass_config):
payload[CONF_STATE_TOPIC] = '{}/{}/{}/state'.format(
discovery_topic, component, object_id)
if ALREADY_DISCOVERED not in hass.data:
hass.data[ALREADY_DISCOVERED] = set()
discovery_hash = (component, object_id)
if discovery_hash in hass.data[ALREADY_DISCOVERED]:
_LOGGER.info("Component has already been discovered: %s %s",
component, object_id)
return
hass.data[ALREADY_DISCOVERED].add(discovery_hash)
_LOGGER.info("Found new component: %s %s", component, object_id)
yield from async_load_platform(
hass, component, platform, payload, hass_config)
+6 -6
View File
@@ -69,8 +69,8 @@ def send_message(hass, message, title=None, data=None):
@asyncio.coroutine
def async_setup(hass, config):
"""Set up the notify services."""
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'))
targets = {}
@@ -97,8 +97,8 @@ def async_setup(hass, config):
notify_service = yield from \
platform.async_get_service(hass, p_config, discovery_info)
elif hasattr(platform, 'get_service'):
notify_service = yield from hass.loop.run_in_executor(
None, platform.get_service, hass, p_config, discovery_info)
notify_service = yield from hass.async_add_job(
platform.get_service, hass, p_config, discovery_info)
else:
raise HomeAssistantError("Invalid notify platform.")
@@ -192,5 +192,5 @@ class BaseNotificationService(object):
kwargs can contain ATTR_TITLE to specify a title.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, partial(self.send_message, message, **kwargs))
return self.hass.async_add_job(
partial(self.send_message, message, **kwargs))
+1 -1
View File
@@ -246,7 +246,7 @@ class HTML5PushCallbackView(HomeAssistantView):
# 2a. If decode is successful, return the payload.
# 2b. If decode is unsuccessful, return a 401.
target_check = jwt.decode(token, verify=False)
target_check = jwt.decode(token, options={'verify_signature': False})
if target_check[ATTR_TARGET] in self.registrations:
possible_target = self.registrations[target_check[ATTR_TARGET]]
key = possible_target[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH]
+1 -1
View File
@@ -85,7 +85,7 @@ class iOSNotificationService(BaseNotificationService):
for target in targets:
if target not in ios.enabled_push_ids():
_LOGGER.error("The target (%s) does not exist in ios.conf.",
_LOGGER.error("The target (%s) does not exist in .ios.conf.",
targets)
return
+29 -4
View File
@@ -15,6 +15,8 @@ from homeassistant.components.notify import (
from homeassistant.const import (CONF_RESOURCE, CONF_METHOD, CONF_NAME)
import homeassistant.helpers.config_validation as cv
CONF_DATA = 'data'
CONF_DATA_TEMPLATE = 'data_template'
CONF_MESSAGE_PARAMETER_NAME = 'message_param_name'
CONF_TARGET_PARAMETER_NAME = 'target_param_name'
CONF_TITLE_PARAMETER_NAME = 'title_param_name'
@@ -34,6 +36,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
default=DEFAULT_TARGET_PARAM_NAME): cv.string,
vol.Optional(CONF_TITLE_PARAMETER_NAME,
default=DEFAULT_TITLE_PARAM_NAME): cv.string,
vol.Optional(CONF_DATA,
default=None): dict,
vol.Optional(CONF_DATA_TEMPLATE,
default=None): {cv.match_all: cv.template_complex}
})
_LOGGER = logging.getLogger(__name__)
@@ -46,23 +52,28 @@ def get_service(hass, config, discovery_info=None):
message_param_name = config.get(CONF_MESSAGE_PARAMETER_NAME)
title_param_name = config.get(CONF_TITLE_PARAMETER_NAME)
target_param_name = config.get(CONF_TARGET_PARAMETER_NAME)
data = config.get(CONF_DATA)
data_template = config.get(CONF_DATA_TEMPLATE)
return RestNotificationService(
resource, method, message_param_name, title_param_name,
target_param_name)
hass, resource, method, message_param_name,
title_param_name, target_param_name, data, data_template)
class RestNotificationService(BaseNotificationService):
"""Implementation of a notification service for REST."""
def __init__(self, resource, method, message_param_name, title_param_name,
target_param_name):
def __init__(self, hass, resource, method, message_param_name,
title_param_name, target_param_name, data, data_template):
"""Initialize the service."""
self._resource = resource
self._hass = hass
self._method = method.upper()
self._message_param_name = message_param_name
self._title_param_name = title_param_name
self._target_param_name = target_param_name
self._data = data
self._data_template = data_template
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
@@ -79,6 +90,20 @@ class RestNotificationService(BaseNotificationService):
# integrations, so just return the first target in the list.
data[self._target_param_name] = kwargs[ATTR_TARGET][0]
if self._data:
data.update(self._data)
elif self._data_template:
def _data_template_creator(value):
"""Recursive template creator helper function."""
if isinstance(value, list):
return [_data_template_creator(item) for item in value]
elif isinstance(value, dict):
return {key: _data_template_creator(item)
for key, item in value.items()}
value.hass = self._hass
return value.async_render(kwargs)
data.update(_data_template_creator(self._data_template))
if self._method == 'POST':
response = requests.post(self._resource, data=data, timeout=10)
elif self._method == 'POST_JSON':
+1 -1
View File
@@ -14,7 +14,7 @@ from homeassistant.const import (
CONF_API_KEY, CONF_USERNAME, CONF_ICON)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['slacker==0.9.42']
REQUIREMENTS = ['slacker==0.9.50']
_LOGGER = logging.getLogger(__name__)
+15 -15
View File
@@ -12,16 +12,17 @@ from email.mime.image import MIMEImage
from email.mime.application import MIMEApplication
import email.utils
import os
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.components.notify import (
ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, PLATFORM_SCHEMA,
BaseNotificationService)
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_TIMEOUT,
CONF_SENDER, CONF_RECIPIENT)
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -42,10 +43,10 @@ DEFAULT_STARTTLS = False
# pylint: disable=no-value-for-parameter
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_RECIPIENT): vol.All(cv.ensure_list, [vol.Email()]),
vol.Required(CONF_SENDER): vol.Email(),
vol.Optional(CONF_SERVER, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_SENDER): vol.Email(),
vol.Optional(CONF_STARTTLS, default=DEFAULT_STARTTLS): cv.boolean,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
@@ -75,11 +76,11 @@ def get_service(hass, config, discovery_info=None):
class MailNotificationService(BaseNotificationService):
"""Implement the notification service for E-Mail messages."""
"""Implement the notification service for E-mail messages."""
def __init__(self, server, port, timeout, sender, starttls, username,
password, recipients, sender_name, debug):
"""Initialize the service."""
"""Initialize the SMTP service."""
self._server = server
self._port = port
self._timeout = timeout
@@ -142,11 +143,11 @@ class MailNotificationService(BaseNotificationService):
if data:
if ATTR_HTML in data:
msg = _build_html_msg(message, data[ATTR_HTML],
images=data.get(ATTR_IMAGES))
msg = _build_html_msg(
message, data[ATTR_HTML], images=data.get(ATTR_IMAGES))
else:
msg = _build_multipart_msg(message,
images=data.get(ATTR_IMAGES))
msg = _build_multipart_msg(
message, images=data.get(ATTR_IMAGES))
else:
msg = _build_text_msg(message)
@@ -167,8 +168,7 @@ class MailNotificationService(BaseNotificationService):
mail = self.connect()
for _ in range(self.tries):
try:
mail.sendmail(self._sender, self.recipients,
msg.as_string())
mail.sendmail(self._sender, self.recipients, msg.as_string())
break
except smtplib.SMTPServerDisconnected:
_LOGGER.warning(
@@ -210,7 +210,7 @@ def _build_multipart_msg(message, images):
msg.attach(attachment)
attachment.add_header('Content-ID', '<{}>'.format(cid))
except TypeError:
_LOGGER.warning("Attachment %s has an unkown MIME type. "
_LOGGER.warning("Attachment %s has an unknown MIME type. "
"Falling back to file", atch_name)
attachment = MIMEApplication(file_bytes, Name=atch_name)
attachment['Content-Disposition'] = ('attachment; '
@@ -226,8 +226,8 @@ def _build_multipart_msg(message, images):
def _build_html_msg(text, html, images):
"""Build Multipart message with in-line images and rich html (UTF-8)."""
_LOGGER.debug("Building html rich email")
"""Build Multipart message with in-line images and rich HTML (UTF-8)."""
_LOGGER.debug("Building HTML rich email")
msg = MIMEMultipart('related')
alternative = MIMEMultipart('alternative')
alternative.attach(MIMEText(text, _charset='utf-8'))
@@ -242,6 +242,6 @@ def _build_html_msg(text, html, images):
msg.attach(attachment)
attachment.add_header('Content-ID', '<{}>'.format(name))
except FileNotFoundError:
_LOGGER.warning('Attachment %s [#%s] not found. Skipping',
_LOGGER.warning("Attachment %s [#%s] not found. Skipping",
atch_name, atch_num)
return msg
+1 -2
View File
@@ -8,7 +8,6 @@ import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import (
ATTR_MESSAGE, ATTR_TITLE, ATTR_DATA, ATTR_TARGET,
PLATFORM_SCHEMA, BaseNotificationService)
@@ -27,7 +26,7 @@ ATTR_DOCUMENT = 'document'
CONF_CHAT_ID = 'chat_id'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_CHAT_ID): cv.positive_int,
vol.Required(CONF_CHAT_ID): vol.Coerce(int),
})
@@ -92,8 +92,8 @@ def async_setup(hass, config):
hass.states.async_set(entity_id, message, attr)
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(DOMAIN, SERVICE_CREATE, create_service,
@@ -33,7 +33,7 @@ from . import purge, migration
from .const import DATA_INSTANCE
from .util import session_scope
REQUIREMENTS = ['sqlalchemy==1.1.9']
REQUIREMENTS = ['sqlalchemy==1.1.10']
_LOGGER = logging.getLogger(__name__)
@@ -44,6 +44,7 @@ DEFAULT_DB_FILE = 'home-assistant_v2.db'
CONF_DB_URL = 'db_url'
CONF_PURGE_DAYS = 'purge_days'
CONF_EVENT_TYPES = 'event_types'
CONNECT_RETRY_WAIT = 3
@@ -51,6 +52,8 @@ FILTER_SCHEMA = vol.Schema({
vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
vol.Optional(CONF_DOMAINS, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EVENT_TYPES, default=[]):
vol.All(cv.ensure_list, [cv.string])
}),
vol.Optional(CONF_INCLUDE, default={}): vol.Schema({
@@ -142,6 +145,7 @@ class Recorder(threading.Thread):
self.include_d = include.get(CONF_DOMAINS, [])
self.exclude = exclude.get(CONF_ENTITIES, []) + \
exclude.get(CONF_DOMAINS, [])
self.exclude_t = exclude.get(CONF_EVENT_TYPES, [])
self.get_session = None
@@ -245,6 +249,9 @@ class Recorder(threading.Thread):
elif event.event_type == EVENT_TIME_CHANGED:
self.queue.task_done()
continue
elif event.event_type in self.exclude_t:
self.queue.task_done()
continue
entity_id = event.data.get(ATTR_ENTITY_ID)
if entity_id is not None:
+24 -7
View File
@@ -26,6 +26,8 @@ _LOGGER = logging.getLogger(__name__)
ATTR_ACTIVITY = 'activity'
ATTR_COMMAND = 'command'
ATTR_DEVICE = 'device'
ATTR_NUM_REPEATS = 'num_repeats'
ATTR_DELAY_SECS = 'delay_secs'
DOMAIN = 'remote'
@@ -40,6 +42,9 @@ SCAN_INTERVAL = timedelta(seconds=30)
SERVICE_SEND_COMMAND = 'send_command'
SERVICE_SYNC = 'sync'
DEFAULT_NUM_REPEATS = '1'
DEFAULT_DELAY_SECS = '0.4'
REMOTE_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
})
@@ -50,7 +55,9 @@ REMOTE_SERVICE_TURN_ON_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({
REMOTE_SERVICE_SEND_COMMAND_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({
vol.Required(ATTR_DEVICE): cv.string,
vol.Required(ATTR_COMMAND): cv.string,
vol.Required(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(ATTR_NUM_REPEATS, default=DEFAULT_NUM_REPEATS): cv.string,
vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): cv.string
})
@@ -74,11 +81,19 @@ def turn_off(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
def send_command(hass, device, command, entity_id=None):
def send_command(hass, device, command, entity_id=None,
num_repeats=None, delay_secs=None):
"""Send a command to a device."""
data = {ATTR_DEVICE: str(device), ATTR_COMMAND: command}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
if num_repeats:
data[ATTR_NUM_REPEATS] = num_repeats
if delay_secs:
data[ATTR_DELAY_SECS] = delay_secs
hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data)
@@ -97,13 +112,16 @@ def async_setup(hass, config):
activity_id = service.data.get(ATTR_ACTIVITY)
device = service.data.get(ATTR_DEVICE)
command = service.data.get(ATTR_COMMAND)
num_repeats = service.data.get(ATTR_NUM_REPEATS)
delay_secs = service.data.get(ATTR_DELAY_SECS)
for remote in target_remotes:
if service.service == SERVICE_TURN_ON:
yield from remote.async_turn_on(activity=activity_id)
elif service.service == SERVICE_SEND_COMMAND:
yield from remote.async_send_command(
device=device, command=command)
device=device, command=command,
num_repeats=num_repeats, delay_secs=delay_secs)
else:
yield from remote.async_turn_off()
@@ -122,8 +140,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'))
hass.services.async_register(
DOMAIN, SERVICE_TURN_OFF, async_handle_remote_service,
@@ -153,5 +171,4 @@ class RemoteDevice(ToggleEntity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.send_command, **kwargs))
return self.hass.async_add_job(ft.partial(self.send_command, **kwargs))
+189 -187
View File
@@ -1,187 +1,189 @@
"""
Support for Harmony Hub devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/remote.harmony/
"""
import logging
from os import path
import urllib.parse
import voluptuous as vol
import homeassistant.components.remote as remote
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, ATTR_ENTITY_ID)
from homeassistant.components.remote import (
PLATFORM_SCHEMA, DOMAIN, ATTR_DEVICE, ATTR_COMMAND, ATTR_ACTIVITY)
from homeassistant.util import slugify
from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['pyharmony==1.0.12']
_LOGGER = logging.getLogger(__name__)
DEFAULT_PORT = 5222
DEVICES = []
SERVICE_SYNC = 'harmony_sync'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Required(ATTR_ACTIVITY, default=None): cv.string,
})
HARMONY_SYNC_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Harmony platform."""
import pyharmony
global DEVICES
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
_LOGGER.debug("Loading Harmony platform: %s", name)
harmony_conf_file = hass.config.path(
'{}{}{}'.format('harmony_', slugify(name), '.conf'))
try:
_LOGGER.debug("Calling pyharmony.ha_get_token for remote at: %s:%s",
host, port)
token = urllib.parse.quote_plus(pyharmony.ha_get_token(host, port))
except ValueError as err:
_LOGGER.warning("%s for remote: %s", err.args[0], name)
return False
_LOGGER.debug("Received token: %s", token)
DEVICES = [HarmonyRemote(
config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT),
config.get(ATTR_ACTIVITY), harmony_conf_file, token)]
add_devices(DEVICES, True)
register_services(hass)
return True
def register_services(hass):
"""Register all services for harmony devices."""
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(
DOMAIN, SERVICE_SYNC, _sync_service, descriptions.get(SERVICE_SYNC),
schema=HARMONY_SYNC_SCHEMA)
def _apply_service(service, service_func, *service_func_args):
"""Handle services to apply."""
entity_ids = service.data.get('entity_id')
if entity_ids:
_devices = [device for device in DEVICES
if device.entity_id in entity_ids]
else:
_devices = DEVICES
for device in _devices:
service_func(device, *service_func_args)
device.schedule_update_ha_state(True)
def _sync_service(service):
_apply_service(service, HarmonyRemote.sync)
class HarmonyRemote(remote.RemoteDevice):
"""Remote representation used to control a Harmony device."""
def __init__(self, name, host, port, activity, out_path, token):
"""Initialize HarmonyRemote class."""
import pyharmony
from pathlib import Path
_LOGGER.debug("HarmonyRemote device init started for: %s", name)
self._name = name
self._ip = host
self._port = port
self._state = None
self._current_activity = None
self._default_activity = activity
self._token = token
self._config_path = out_path
_LOGGER.debug("Retrieving harmony config using token: %s", token)
self._config = pyharmony.ha_get_config(self._token, host, port)
if not Path(self._config_path).is_file():
_LOGGER.debug("Writing harmony configuration to file: %s",
out_path)
pyharmony.ha_write_config_file(self._config, self._config_path)
@property
def name(self):
"""Return the Harmony device's name."""
return self._name
@property
def device_state_attributes(self):
"""Add platform specific attributes."""
return {'current_activity': self._current_activity}
@property
def is_on(self):
"""Return False if PowerOff is the current activity, otherwise True."""
return self._current_activity != 'PowerOff'
def update(self):
"""Return current activity."""
import pyharmony
name = self._name
_LOGGER.debug("Polling %s for current activity", name)
state = pyharmony.ha_get_current_activity(
self._token, self._config, self._ip, self._port)
_LOGGER.debug("%s current activity reported as: %s", name, state)
self._current_activity = state
self._state = bool(state != 'PowerOff')
def turn_on(self, **kwargs):
"""Start an activity from the Harmony device."""
import pyharmony
if kwargs[ATTR_ACTIVITY]:
activity = kwargs[ATTR_ACTIVITY]
else:
activity = self._default_activity
if activity:
pyharmony.ha_start_activity(
self._token, self._ip, self._port, self._config, activity)
self._state = True
else:
_LOGGER.error("No activity specified with turn_on service")
def turn_off(self):
"""Start the PowerOff activity."""
import pyharmony
pyharmony.ha_power_off(self._token, self._ip, self._port)
def send_command(self, **kwargs):
"""Send a command to one device."""
import pyharmony
pyharmony.ha_send_command(
self._token, self._ip, self._port, kwargs[ATTR_DEVICE],
kwargs[ATTR_COMMAND])
def sync(self):
"""Sync the Harmony device with the web service."""
import pyharmony
_LOGGER.debug("Syncing hub with Harmony servers")
pyharmony.ha_sync(self._token, self._ip, self._port)
self._config = pyharmony.ha_get_config(
self._token, self._ip, self._port)
_LOGGER.debug("Writing hub config to file: %s", self._config_path)
pyharmony.ha_write_config_file(self._config, self._config_path)
"""
Support for Harmony Hub devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/remote.harmony/
"""
import logging
from os import path
import urllib.parse
import voluptuous as vol
import homeassistant.components.remote as remote
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, ATTR_ENTITY_ID)
from homeassistant.components.remote import (
PLATFORM_SCHEMA, DOMAIN, ATTR_DEVICE, ATTR_COMMAND,
ATTR_ACTIVITY, ATTR_NUM_REPEATS, ATTR_DELAY_SECS)
from homeassistant.util import slugify
from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['pyharmony==1.0.16']
_LOGGER = logging.getLogger(__name__)
DEFAULT_PORT = 5222
DEVICES = []
SERVICE_SYNC = 'harmony_sync'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Required(ATTR_ACTIVITY, default=None): cv.string,
})
HARMONY_SYNC_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Harmony platform."""
import pyharmony
global DEVICES
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
_LOGGER.debug("Loading Harmony platform: %s", name)
harmony_conf_file = hass.config.path(
'{}{}{}'.format('harmony_', slugify(name), '.conf'))
try:
_LOGGER.debug("Calling pyharmony.ha_get_token for remote at: %s:%s",
host, port)
token = urllib.parse.quote_plus(pyharmony.ha_get_token(host, port))
except ValueError as err:
_LOGGER.warning("%s for remote: %s", err.args[0], name)
return False
_LOGGER.debug("Received token: %s", token)
DEVICES = [HarmonyRemote(
config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT),
config.get(ATTR_ACTIVITY), harmony_conf_file, token)]
add_devices(DEVICES, True)
register_services(hass)
return True
def register_services(hass):
"""Register all services for harmony devices."""
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(
DOMAIN, SERVICE_SYNC, _sync_service, descriptions.get(SERVICE_SYNC),
schema=HARMONY_SYNC_SCHEMA)
def _apply_service(service, service_func, *service_func_args):
"""Handle services to apply."""
entity_ids = service.data.get('entity_id')
if entity_ids:
_devices = [device for device in DEVICES
if device.entity_id in entity_ids]
else:
_devices = DEVICES
for device in _devices:
service_func(device, *service_func_args)
device.schedule_update_ha_state(True)
def _sync_service(service):
_apply_service(service, HarmonyRemote.sync)
class HarmonyRemote(remote.RemoteDevice):
"""Remote representation used to control a Harmony device."""
def __init__(self, name, host, port, activity, out_path, token):
"""Initialize HarmonyRemote class."""
import pyharmony
from pathlib import Path
_LOGGER.debug("HarmonyRemote device init started for: %s", name)
self._name = name
self._ip = host
self._port = port
self._state = None
self._current_activity = None
self._default_activity = activity
self._token = token
self._config_path = out_path
_LOGGER.debug("Retrieving harmony config using token: %s", token)
self._config = pyharmony.ha_get_config(self._token, host, port)
if not Path(self._config_path).is_file():
_LOGGER.debug("Writing harmony configuration to file: %s",
out_path)
pyharmony.ha_write_config_file(self._config, self._config_path)
@property
def name(self):
"""Return the Harmony device's name."""
return self._name
@property
def device_state_attributes(self):
"""Add platform specific attributes."""
return {'current_activity': self._current_activity}
@property
def is_on(self):
"""Return False if PowerOff is the current activity, otherwise True."""
return self._current_activity != 'PowerOff'
def update(self):
"""Return current activity."""
import pyharmony
name = self._name
_LOGGER.debug("Polling %s for current activity", name)
state = pyharmony.ha_get_current_activity(
self._token, self._config, self._ip, self._port)
_LOGGER.debug("%s current activity reported as: %s", name, state)
self._current_activity = state
self._state = bool(state != 'PowerOff')
def turn_on(self, **kwargs):
"""Start an activity from the Harmony device."""
import pyharmony
if kwargs[ATTR_ACTIVITY]:
activity = kwargs[ATTR_ACTIVITY]
else:
activity = self._default_activity
if activity:
pyharmony.ha_start_activity(
self._token, self._ip, self._port, self._config, activity)
self._state = True
else:
_LOGGER.error("No activity specified with turn_on service")
def turn_off(self):
"""Start the PowerOff activity."""
import pyharmony
pyharmony.ha_power_off(self._token, self._ip, self._port)
def send_command(self, **kwargs):
"""Send a set of commands to one device."""
import pyharmony
pyharmony.ha_send_commands(
self._token, self._ip, self._port, kwargs[ATTR_DEVICE],
kwargs[ATTR_COMMAND], int(kwargs[ATTR_NUM_REPEATS]),
float(kwargs[ATTR_DELAY_SECS]))
def sync(self):
"""Sync the Harmony device with the web service."""
import pyharmony
_LOGGER.debug("Syncing hub with Harmony servers")
pyharmony.ha_sync(self._token, self._ip, self._port)
self._config = pyharmony.ha_get_config(
self._token, self._ip, self._port)
_LOGGER.debug("Writing hub config to file: %s", self._config_path)
pyharmony.ha_write_config_file(self._config, self._config_path)
+2 -1
View File
@@ -104,7 +104,8 @@ class ITachIP2IRRemote(remote.RemoteDevice):
def send_command(self, **kwargs):
"""Send a command to one device."""
self.itachip2ir.send(self._name, kwargs[ATTR_COMMAND], 1)
for command in kwargs[ATTR_COMMAND]:
self.itachip2ir.send(self._name, command, 1)
def update(self):
"""Update the device."""
+6 -7
View File
@@ -64,16 +64,15 @@ class KiraRemote(Entity):
def send_command(self, **kwargs):
"""Send a command to one device."""
code_tuple = (kwargs.get(remote.ATTR_COMMAND),
kwargs.get(remote.ATTR_DEVICE))
_LOGGER.info("Sending Command: %s to %s", *code_tuple)
self._kira.sendCode(code_tuple)
for command in kwargs.get(remote.ATTR_COMMAND):
code_tuple = (command,
kwargs.get(remote.ATTR_DEVICE))
_LOGGER.info("Sending Command: %s to %s", *code_tuple)
self._kira.sendCode(code_tuple)
def async_send_command(self, **kwargs):
"""Send a command to a device.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.send_command, **kwargs))
return self.hass.async_add_job(ft.partial(self.send_command, **kwargs))
@@ -30,8 +30,14 @@ send_command:
description: Device ID to send command to
example: '32756745'
command:
description: Command to send
description: A single command or a list of commands to send.
example: 'Play'
num_repeats:
description: An optional value that specifies the number of times you want to repeat the command(s). If not specified, the command(s) will not be repeated
example: '5'
delay_secs:
description: An optional value that specifies that number of seconds you want to wait in between repeated commands. If not specified, the default of 0.4 seconds will be used
example: '0.75'
harmony_sync:
description: Syncs the remote's configuration
+1 -1
View File
@@ -371,7 +371,7 @@ class RflinkCommand(RflinkDevice):
# Rflink protocol/transport handles asynchronous writing of buffer
# to serial/tcp device. Does not wait for command send
# confirmation.
self.hass.loop.run_in_executor(None, ft.partial(
self.hass.async_add_job(ft.partial(
self._protocol.send_command, self._device_id, cmd))
if repetitions > 1:

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