Compare commits

...

142 Commits

Author SHA1 Message Date
Fabian Affolter 70ea16bdc0 Merge pull request #7648 from home-assistant/release-0-45
0.45
2017-05-21 00:34:44 +02:00
Marcelo Moreira de Mello 943958b140 Added support to Amcrest camera to feed using RTSP via ffmpeg (#7646)
* Implemented ffmpeg option on Amcrest camera and upgraded to version 1.2.0

* Added ffmpeg arguments and binary options to Amcrest camera

* Added ffmpeg as dependencies

* Makes lint happy and fixed requirements_all.txt

* Inherent the ffmpeg.binary configuration from ffmpeg component

* Update amcrest.py
2017-05-20 23:55:15 +02:00
John Arild Berentsen 23c5fc0aad Bugfix #7586 (#7661) 2017-05-20 23:53:48 +02:00
Fabian Affolter 45b4ef46cc Align with OpenALPR platform for naming conf variables (#7650) 2017-05-20 23:51:16 +02:00
Paulus Schoutsen de85d38aa5 Update frontend 2017-05-20 08:08:06 -07:00
Robbie Trencheny c4da921cb5 Add network_key as a config option (#7637)
* Add network_key as a config option

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

# The 2nd commit message will be skipped:

#	unused import

# The 3rd commit message will be skipped:

#	Use get for config

* Add seperate zwave panel

* Modify set_config_parameter to accept setting string values

* descriptions

* Tweaks

* Tweaks

* Tweaks

* Tweaks

* lint

* Fallback if no config parameteres are available

* Update services.yaml

* review changes
2017-05-18 17:41:31 -07:00
Paulus Schoutsen 6662b7f52d Update frontend 2017-05-18 17:41:26 -07:00
Paulus Schoutsen eb473600f6 Version bump to 0.45 2017-05-17 23:03:27 -07:00
Paulus Schoutsen de999d8439 Merge remote-tracking branch 'origin/master' into dev 2017-05-17 23:03:00 -07:00
Paulus Schoutsen 6d97546f40 Update frontend 2017-05-17 22:29:46 -07:00
Paulus Schoutsen e773133bcf Fix automation failing to setup if no automations specified (#7647) 2017-05-17 21:57:50 -07:00
Anders Melchiorsen 3d4b2436db Coerce color_temp to int even when passed in as kelvin (#7640) 2017-05-17 19:20:59 -07:00
Paulus Schoutsen a068efcd47 Abort tests when instances leaked (#7623) 2017-05-18 00:19:40 +02:00
Fabian Affolter f86edd4f24 Seven segments OCR image processing (#7632)
* Add initial seven segments OCR image processing

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

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

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

* no need for the `kodi_execute_addon` service

* unused import

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

* Fixed styleguide problems

* Fix style problems

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

* Refactor into preprocess_turn_on_alternatives

* LIFX: use light.preprocess_turn_on_alternatives

This avoids the color_name duplication and gains support for profile.

* Add kelvin parameter to light.turn_on

* Add brightness_pct parameter to light.turn_on

* LIFX: accept brightness_pct in effects

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

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

* Use default keyword instead.

* Use list instead of tuple

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

* Use None and return

* Remove I/O

* Use less memory

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

* Add tests and break lines

* Fix style errors

* Make test work with python<3.6

* Fix last two pylint errors

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

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

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

* `sender_name` instead of product_name

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

* Fix outside task message sending

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

* No longer require cast during tests

* No longer required dependency for tests

* No longer require pymochad for tests

* Astral is a core dependency

* Avoid having to install datadog dependency during tests

* CMUS test doesn't test anything

* Frontier Silicon doesn't test anything

* No longer require mutagen

* Update requirements_test_all.txt

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

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

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

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

* fix line too long

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

* requested changes

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

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

* Simplify patching

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

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

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

* Update sun position before emitting initial update

* Simplify based on armills comment.

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

* Updated .coveragerc to ommit raspihats platforms.

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

* Add lifx_set_color

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

Thus, turn_on can be implemented just by asking.

* Rename set_color to set_state

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

* Added Axis logo to static images

* Added Axis logo to configurator
Added Axis mdns discovery

* Fixed flake8 and pylint comments

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

* Added dependencies to requirements_all.txt

* Clean up

* Added files to coveragerc

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

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

* Don't call update in the constructor

* Don't keep hass private

* Unnecessary lint ignore, following Baloobs suggestion of using NotImplementedError

* Axis package not in pypi yet

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

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

* Don't expect axis.conf contains correct values

* Improved configuration validation

* Trigger time better explains functionality than scan interval

* Forgot to remove this earlier

* Guideline says double qoutes for sentences

* Return false from discovery if config file contains bad data

* Keys in AXIS_DEVICES are serialnumber

* Ordered imports in alphabetical order

* Moved requirement to pypi

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

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

* Removed unnecessary configurator steps

* Changed link in configurator to platform documentation

* Add not-context-manager (#7523)

* Add not-context-manager

* Add missing comma

* Threadsafe configurator (#7536)

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

* No blank lines allowed after function docstring

* Fix comment Tox

* Added Axis hub, binary sensors and camera

* Added Axis logo to static images

* Added Axis logo to configurator
Added Axis mdns discovery

* Fixed flake8 and pylint comments

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

* Added dependencies to requirements_all.txt

* Clean up

* Added files to coveragerc

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

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

* Don't call update in the constructor

* Don't keep hass private

* Unnecessary lint ignore, following Baloobs suggestion of using NotImplementedError

* Axis package not in pypi yet

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

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

* Don't expect axis.conf contains correct values

* Improved configuration validation

* Trigger time better explains functionality than scan interval

* Forgot to remove this earlier

* Guideline says double qoutes for sentences

* Return false from discovery if config file contains bad data

* Keys in AXIS_DEVICES are serialnumber

* Ordered imports in alphabetical order

* Moved requirement to pypi

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

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

* Removed unnecessary configurator steps

* Changed link in configurator to platform documentation

* No blank lines allowed after function docstring

* No blank lines allowed after function docstring

* Changed discovery to use axis instead of axis_mdns

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

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

* Fix unittests

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

* Comment out blinkt because it depends on GPIO

* Add pip upgrade check back

* Disable import error blinkt

* Update comment

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

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

* Replace STATIC_TIME with datetime object check

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

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

* No blank lines allowed after function docstring

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

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

* Fixed some code style issues

* Removed global listener

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

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

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

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

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

# The 2nd commit message will be skipped:

#	unused import

# The 3rd commit message will be skipped:

#	Use get for config

* Add seperate zwave panel

* more info

* Add usercodeview

* Improve api

* Improve api

* Separate api into own file.

* disable missing import

* review changes

* Tests 1

* Verify that we fetch data from groups

* Tests groups

* config 1

* usercode 1

* Api mods

* Tweak API

* docstrings

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

* Build fromtend

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

* Convert datetimes to dates for astral

* Fix tests for updated code

* Fix times now that calcs are fixed

* Move sun functions to helpers

* Fix flake on new file

* Additional tweaks from review

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

For completeness, the initial hue is also changed to a float.
2017-05-08 16:51:27 +02:00
Mitesh Patel 66cbdc3043 Uses pypi for deps (#7485) 2017-05-07 17:32:13 -07:00
Paulus Schoutsen e1d1385358 Fix travis 2017-05-07 16:55:22 -07:00
Paulus Schoutsen bafc04ca42 Update tox.ini 2017-05-07 16:53:28 -07:00
Paulus Schoutsen 5717c87097 Update tox.ini 2017-05-07 16:51:46 -07:00
Paulus Schoutsen 00ec50da4b Update frontend 2017-05-07 13:50:07 -07:00
Marc Egli c1056ea4d4 Fix plant MIN_TEMPERATURE, MAX_TEMPERATURE validation (#7476)
* Fix plant MIN_TEMPERATURE, MAX_TEMPERATURE validation

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

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

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

* Update tox

* Update validate

* Lint

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

* Switch pymysensors to pypi

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

* Improve test_invalid_config datadog test

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

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

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

* Install python-openzwave from PyPI

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

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

* Add python-openzwave deps to .travis.yml

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

Thanks to @MartinHjelmare for this fix.

* Update docker build for PyPI openzwave

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

* added raspihats to COMMENT_REQUIREMENTS in gen_requirements_all.py

* disabled pylint import errors

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

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

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

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

For example, aborting an effect by selecting a new brightness could keep a
color set by the effect.
2017-05-04 00:21:24 +02:00
Brian Cribbs 1b2c83145c fixing nits 2017-05-02 15:41:45 -04:00
Brian Cribbs 098e28534b fixing documentation 2017-05-01 13:34:34 -04:00
Brian Cribbs dc716cd971 repairing functionality for non-zero based ranges 2017-05-01 13:22:54 -04:00
Jack Minardi 7a24e210ae Try again to pass string to error msg 2017-05-01 09:31:23 -04:00
Jack Minardi bc0559813c Dont add two strings inside logger call 2017-04-30 22:26:16 -04:00
Jack Minardi dd7690f265 Use % formatting 2017-04-30 21:31:55 -04:00
Jack Minardi b6827ce57a Use throwaray variable name 2017-04-30 21:02:03 -04:00
Jack Minardi 8bf1c21738 Add space 2017-04-25 00:48:23 -04:00
Jack Minardi 943861a8a3 Remove unused var 2017-04-25 00:45:59 -04:00
Jack Minardi 450fd7f2b5 Log out of router admin interface after devices are recorded. 2017-04-25 00:34:26 -04:00
Jack Minardi 2d5da3e958 Catch KeyError; Add response.text to error message 2017-04-25 00:32:31 -04:00
200 changed files with 7412 additions and 2099 deletions
+14
View File
@@ -20,6 +20,9 @@ omit =
homeassistant/components/android_ip_webcam.py
homeassistant/components/*/android_ip_webcam.py
homeassistant/components/axis.py
homeassistant/components/*/axis.py
homeassistant/components/bbb_gpio.py
homeassistant/components/*/bbb_gpio.py
@@ -59,6 +62,9 @@ omit =
homeassistant/components/isy994.py
homeassistant/components/*/isy994.py
homeassistant/components/kira.py
homeassistant/components/*/kira.py
homeassistant/components/lutron.py
homeassistant/components/*/lutron.py
@@ -83,12 +89,18 @@ omit =
homeassistant/components/qwikswitch.py
homeassistant/components/*/qwikswitch.py
homeassistant/components/raspihats.py
homeassistant/components/*/raspihats.py
homeassistant/components/rfxtrx.py
homeassistant/components/*/rfxtrx.py
homeassistant/components/rpi_gpio.py
homeassistant/components/*/rpi_gpio.py
homeassistant/components/rpi_pfio.py
homeassistant/components/*/rpi_pfio.py
homeassistant/components/scsgate.py
homeassistant/components/*/scsgate.py
@@ -175,6 +187,7 @@ omit =
homeassistant/components/binary_sensor/flic.py
homeassistant/components/binary_sensor/hikvision.py
homeassistant/components/binary_sensor/iss.py
homeassistant/components/binary_sensor/mystrom.py
homeassistant/components/binary_sensor/pilight.py
homeassistant/components/binary_sensor/ping.py
homeassistant/components/binary_sensor/rest.py
@@ -239,6 +252,7 @@ omit =
homeassistant/components/ifttt.py
homeassistant/components/image_processing/dlib_face_detect.py
homeassistant/components/image_processing/dlib_face_identify.py
homeassistant/components/image_processing/seven_segments.py
homeassistant/components/joaoapps_join.py
homeassistant/components/keyboard.py
homeassistant/components/keyboard_remote.py
+9 -5
View File
@@ -1,13 +1,15 @@
sudo: false
addons:
apt:
packages:
- libudev-dev
matrix:
fast_finish: true
include:
- python: "3.4.2"
env: TOXENV=py34
- python: "3.4.2"
env: TOXENV=requirements
- python: "3.4.2"
env: TOXENV=lint
- python: "3.4.2"
env: TOXENV=py34
# - python: "3.5"
# env: TOXENV=typing
- python: "3.5"
@@ -16,6 +18,8 @@ matrix:
env: TOXENV=py36
- python: "3.6-dev"
env: TOXENV=py36
- python: "3.4.2"
env: TOXENV=requirements
# allow_failures:
# - python: "3.5"
# env: TOXENV=typing
@@ -25,5 +29,5 @@ cache:
- $HOME/.cache/pip
install: pip install -U tox coveralls
language: python
script: tox
script: travis_wait tox
after_success: coveralls
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.5
FROM python:3.6
MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
# Uncomment any of the following lines to disable the installation.
@@ -4,6 +4,7 @@ Interfaces with Wink Cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.wink/
"""
import asyncio
import logging
import homeassistant.components.alarm_control_panel as alarm
@@ -42,6 +43,11 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
"""Initialize the Wink alarm."""
super().__init__(wink, hass)
@asyncio.coroutine
def async_added_to_hass(self):
"""Callback when entity is added to hass."""
self.hass.data[DOMAIN]['entities']['alarm_control_panel'].append(self)
@property
def state(self):
"""Return the state of the device."""
+1 -7
View File
@@ -17,7 +17,6 @@ from homeassistant.core import callback
from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import template, script, config_validation as cv
from homeassistant.components.http import HomeAssistantView
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -36,7 +35,6 @@ CONF_TEXT = 'text'
CONF_FLASH_BRIEFINGS = 'flash_briefings'
CONF_UID = 'uid'
CONF_DATE = 'date'
CONF_TITLE = 'title'
CONF_AUDIO = 'audio'
CONF_TEXT = 'text'
@@ -88,7 +86,6 @@ CONFIG_SCHEMA = vol.Schema({
CONF_FLASH_BRIEFINGS: {
cv.string: vol.All(cv.ensure_list, [{
vol.Required(CONF_UID, default=str(uuid.uuid4())): cv.string,
vol.Optional(CONF_DATE, default=datetime.utcnow()): cv.string,
vol.Required(CONF_TITLE): cv.template,
vol.Optional(CONF_AUDIO): cv.template,
vol.Required(CONF_TEXT, default=""): cv.template,
@@ -331,10 +328,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
else:
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)
if isinstance(item[CONF_DATE], str):
item[CONF_DATE] = dt_util.parse_datetime(item[CONF_DATE])
output[ATTR_UPDATE_DATE] = item[CONF_DATE].strftime(DATE_FORMAT)
output[ATTR_UPDATE_DATE] = datetime.now().strftime(DATE_FORMAT)
briefing.append(output)
+33 -11
View File
@@ -16,7 +16,7 @@ from homeassistant.core import CoreState
from homeassistant import config as conf_util
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START)
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID)
from homeassistant.components import logbook
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import extract_domain_configs, script, condition
@@ -26,6 +26,7 @@ from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.loader import get_platform
from homeassistant.util.dt import utcnow
import homeassistant.helpers.config_validation as cv
from homeassistant.components.frontend import register_built_in_panel
DOMAIN = 'automation'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
@@ -81,6 +82,8 @@ _TRIGGER_SCHEMA = vol.All(
_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
PLATFORM_SCHEMA = vol.Schema({
# str on purpose
CONF_ID: str,
CONF_ALIAS: cv.string,
vol.Optional(CONF_INITIAL_STATE): cv.boolean,
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
@@ -139,16 +142,21 @@ def reload(hass):
hass.services.call(DOMAIN, SERVICE_RELOAD)
def async_reload(hass):
"""Reload the automation from config.
Returns a coroutine object.
"""
return hass.services.async_call(DOMAIN, SERVICE_RELOAD)
@asyncio.coroutine
def async_setup(hass, config):
"""Set up the automation."""
component = EntityComponent(_LOGGER, DOMAIN, hass,
group_name=GROUP_NAME_ALL_AUTOMATIONS)
success = yield from _async_process_config(hass, config, component)
if not success:
return False
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(
@@ -215,15 +223,20 @@ def async_setup(hass, config):
DOMAIN, service, turn_onoff_service_handler,
descriptions.get(service), schema=SERVICE_SCHEMA)
if 'frontend' in hass.config.components:
register_built_in_panel(hass, 'automation', 'Automations',
'mdi:playlist-play')
return True
class AutomationEntity(ToggleEntity):
"""Entity to show status of entity."""
def __init__(self, name, async_attach_triggers, cond_func, async_action,
hidden, initial_state):
def __init__(self, automation_id, name, async_attach_triggers, cond_func,
async_action, hidden, initial_state):
"""Initialize an automation entity."""
self._id = automation_id
self._name = name
self._async_attach_triggers = async_attach_triggers
self._async_detach_triggers = None
@@ -346,6 +359,16 @@ class AutomationEntity(ToggleEntity):
self.async_trigger)
yield from self.async_update_ha_state()
@property
def device_state_attributes(self):
"""Return automation attributes."""
if self._id is None:
return None
return {
CONF_ID: self._id
}
@asyncio.coroutine
def _async_process_config(hass, config, component):
@@ -359,6 +382,7 @@ def _async_process_config(hass, config, component):
conf = config[config_key]
for list_no, config_block in enumerate(conf):
automation_id = config_block.get(CONF_ID)
name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key,
list_no)
@@ -383,16 +407,14 @@ def _async_process_config(hass, config, component):
config_block.get(CONF_TRIGGER, []), name
)
entity = AutomationEntity(
name, async_attach_triggers, cond_func, action, hidden,
initial_state)
automation_id, name, async_attach_triggers, cond_func, action,
hidden, initial_state)
entities.append(entity)
if entities:
yield from component.async_add_entities(entities)
return len(entities) > 0
def _async_get_action(hass, config, name):
"""Return an action based on a configuration."""
+2 -15
View File
@@ -9,8 +9,8 @@ import logging
import voluptuous as vol
from homeassistant.core import callback, CoreState
from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_START
from homeassistant.core import callback
from homeassistant.const import CONF_PLATFORM
from homeassistant.helpers import config_validation as cv
CONF_EVENT_TYPE = 'event_type'
@@ -31,19 +31,6 @@ def async_trigger(hass, config, action):
event_type = config.get(CONF_EVENT_TYPE)
event_data = config.get(CONF_EVENT_DATA)
if (event_type == EVENT_HOMEASSISTANT_START and
hass.state == CoreState.starting):
_LOGGER.warning('Deprecation: Automations should not listen to event '
"'homeassistant_start'. Use platform 'homeassistant' "
'instead. Feature will be removed in 0.45')
hass.async_run_job(action, {
'trigger': {
'platform': 'event',
'event': None,
},
})
return lambda: None
@callback
def handle_event(event):
"""Listen for events and calls the action when data matches."""
@@ -79,6 +79,10 @@ def async_trigger(hass, config, action):
call_action()
return
# If only state attributes changed, ignore this event
if from_s.last_changed == to_s.last_changed:
return
@callback
def state_for_listener(now):
"""Fire on state changes after a delay and calls action."""
@@ -16,8 +16,6 @@ from homeassistant.const import (
from homeassistant.helpers.event import async_track_sunrise, async_track_sunset
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['sun']
_LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.Schema({
+314
View File
@@ -0,0 +1,314 @@
"""
Support for Axis devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/axis/
"""
import json
import logging
import os
import voluptuous as vol
from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
CONF_HOST, CONF_INCLUDE, CONF_NAME,
CONF_PASSWORD, CONF_TRIGGER_TIME,
CONF_USERNAME, EVENT_HOMEASSISTANT_STOP)
from homeassistant.components.discovery import SERVICE_AXIS
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
REQUIREMENTS = ['axis==7']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'axis'
CONFIG_FILE = 'axis.conf'
AXIS_DEVICES = {}
EVENT_TYPES = ['motion', 'vmd3', 'pir', 'sound',
'daynight', 'tampering', 'input']
PLATFORMS = ['camera']
AXIS_INCLUDE = EVENT_TYPES + PLATFORMS
AXIS_DEFAULT_HOST = '192.168.0.90'
AXIS_DEFAULT_USERNAME = 'root'
AXIS_DEFAULT_PASSWORD = 'pass'
DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_INCLUDE):
vol.All(cv.ensure_list, [vol.In(AXIS_INCLUDE)]),
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_HOST, default=AXIS_DEFAULT_HOST): cv.string,
vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_TRIGGER_TIME, default=0): cv.positive_int,
vol.Optional(ATTR_LOCATION, default=''): cv.string,
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: DEVICE_SCHEMA,
}),
}, extra=vol.ALLOW_EXTRA)
def request_configuration(hass, name, host, serialnumber):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
def configuration_callback(callback_data):
"""Called when config is submitted."""
if CONF_INCLUDE not in callback_data:
configurator.notify_errors(request_id,
"Functionality mandatory.")
return False
callback_data[CONF_INCLUDE] = callback_data[CONF_INCLUDE].split()
callback_data[CONF_HOST] = host
if CONF_NAME not in callback_data:
callback_data[CONF_NAME] = name
try:
config = DEVICE_SCHEMA(callback_data)
except vol.Invalid:
configurator.notify_errors(request_id,
"Bad input, please check spelling.")
return False
if setup_device(hass, config):
config_file = _read_config(hass)
config_file[serialnumber] = dict(config)
del config_file[serialnumber]['hass']
_write_config(hass, config_file)
configurator.request_done(request_id)
else:
configurator.notify_errors(request_id,
"Failed to register, please try again.")
return False
title = '{} ({})'.format(name, host)
request_id = configurator.request_config(
hass, title, configuration_callback,
description='Functionality: ' + str(AXIS_INCLUDE),
entity_picture="/static/images/logo_axis.png",
link_name='Axis platform documentation',
link_url='https://home-assistant.io/components/axis/',
submit_caption="Confirm",
fields=[
{'id': CONF_NAME,
'name': "Device name",
'type': 'text'},
{'id': CONF_USERNAME,
'name': "User name",
'type': 'text'},
{'id': CONF_PASSWORD,
'name': 'Password',
'type': 'password'},
{'id': CONF_INCLUDE,
'name': "Device functionality (space separated list)",
'type': 'text'},
{'id': ATTR_LOCATION,
'name': "Physical location of device (optional)",
'type': 'text'},
{'id': CONF_TRIGGER_TIME,
'name': "Sensor update interval (optional)",
'type': 'number'},
]
)
def setup(hass, base_config):
"""Common setup for Axis devices."""
def _shutdown(call): # pylint: disable=unused-argument
"""Stop the metadatastream on shutdown."""
for serialnumber, device in AXIS_DEVICES.items():
_LOGGER.info("Stopping metadatastream for %s.", serialnumber)
device.stop_metadatastream()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
def axis_device_discovered(service, discovery_info):
"""Called when axis devices has been found."""
host = discovery_info['host']
name = discovery_info['hostname']
serialnumber = discovery_info['properties']['macaddress']
if serialnumber not in AXIS_DEVICES:
config_file = _read_config(hass)
if serialnumber in config_file:
try:
config = DEVICE_SCHEMA(config_file[serialnumber])
except vol.Invalid as err:
_LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err)
return False
if not setup_device(hass, config):
_LOGGER.error("Couldn\'t set up %s", config['name'])
else:
request_configuration(hass, name, host, serialnumber)
discovery.listen(hass, SERVICE_AXIS, axis_device_discovered)
if DOMAIN in base_config:
for device in base_config[DOMAIN]:
config = base_config[DOMAIN][device]
if CONF_NAME not in config:
config[CONF_NAME] = device
if not setup_device(hass, config):
_LOGGER.error("Couldn\'t set up %s", config['name'])
return True
def setup_device(hass, config):
"""Set up device."""
from axis import AxisDevice
config['hass'] = hass
device = AxisDevice(config) # Initialize device
enable_metadatastream = False
if device.serial_number is None:
# If there is no serial number a connection could not be made
_LOGGER.error("Couldn\'t connect to %s", config[CONF_HOST])
return False
for component in config[CONF_INCLUDE]:
if component in EVENT_TYPES:
# Sensors are created by device calling event_initialized
# when receiving initialize messages on metadatastream
device.add_event_topic(convert(component, 'type', 'subscribe'))
if not enable_metadatastream:
enable_metadatastream = True
else:
discovery.load_platform(hass, component, DOMAIN, config)
if enable_metadatastream:
device.initialize_new_event = event_initialized
device.initiate_metadatastream()
AXIS_DEVICES[device.serial_number] = device
return True
def _read_config(hass):
"""Read Axis config."""
path = hass.config.path(CONFIG_FILE)
if not os.path.isfile(path):
return {}
with open(path) as f_handle:
# Guard against empty file
return json.loads(f_handle.read() or '{}')
def _write_config(hass, config):
"""Write Axis config."""
data = json.dumps(config)
with open(hass.config.path(CONFIG_FILE), 'w', encoding='utf-8') as outfile:
outfile.write(data)
def event_initialized(event):
"""Register event initialized on metadatastream here."""
hass = event.device_config('hass')
discovery.load_platform(hass,
convert(event.topic, 'topic', 'platform'),
DOMAIN, {'axis_event': event})
class AxisDeviceEvent(Entity):
"""Representation of a Axis device event."""
def __init__(self, axis_event):
"""Initialize the event."""
self.axis_event = axis_event
self._event_class = convert(self.axis_event.topic, 'topic', 'class')
self._name = '{}_{}_{}'.format(self.axis_event.device_name,
convert(self.axis_event.topic,
'topic', 'type'),
self.axis_event.id)
self.axis_event.callback = self._update_callback
def _update_callback(self):
"""Update the sensor's state, if needed."""
self.update()
self.schedule_update_ha_state()
@property
def name(self):
"""Return the name of the event."""
return self._name
@property
def device_class(self):
"""Return the class of the event."""
return self._event_class
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def device_state_attributes(self):
"""Return the state attributes of the event."""
attr = {}
tripped = self.axis_event.is_tripped
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
location = self.axis_event.device_config(ATTR_LOCATION)
if location:
attr[ATTR_LOCATION] = location
return attr
def convert(item, from_key, to_key):
"""Translate between Axis and HASS syntax."""
for entry in REMAP:
if entry[from_key] == item:
return entry[to_key]
REMAP = [{'type': 'motion',
'class': 'motion',
'topic': 'tns1:VideoAnalytics/tnsaxis:MotionDetection',
'subscribe': 'onvif:VideoAnalytics/axis:MotionDetection',
'platform': 'binary_sensor'},
{'type': 'vmd3',
'class': 'motion',
'topic': 'tns1:RuleEngine/tnsaxis:VMD3/vmd3_video_1',
'subscribe': 'onvif:RuleEngine/axis:VMD3/vmd3_video_1',
'platform': 'binary_sensor'},
{'type': 'pir',
'class': 'motion',
'topic': 'tns1:Device/tnsaxis:Sensor/PIR',
'subscribe': 'onvif:Device/axis:Sensor/axis:PIR',
'platform': 'binary_sensor'},
{'type': 'sound',
'class': 'sound',
'topic': 'tns1:AudioSource/tnsaxis:TriggerLevel',
'subscribe': 'onvif:AudioSource/axis:TriggerLevel',
'platform': 'binary_sensor'},
{'type': 'daynight',
'class': 'light',
'topic': 'tns1:VideoSource/tnsaxis:DayNightVision',
'subscribe': 'onvif:VideoSource/axis:DayNightVision',
'platform': 'binary_sensor'},
{'type': 'tampering',
'class': 'safety',
'topic': 'tns1:VideoSource/tnsaxis:Tampering',
'subscribe': 'onvif:VideoSource/axis:Tampering',
'platform': 'binary_sensor'},
{'type': 'input',
'class': 'input',
'topic': 'tns1:Device/tnsaxis:IO/Port',
'subscribe': 'onvif:Device/axis:IO/Port',
'platform': 'sensor'}, ]
@@ -0,0 +1,68 @@
"""
Support for Axis binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.axis/
"""
import logging
from datetime import timedelta
from homeassistant.components.binary_sensor import (BinarySensorDevice)
from homeassistant.components.axis import (AxisDeviceEvent)
from homeassistant.const import (CONF_TRIGGER_TIME)
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.util.dt import utcnow
DEPENDENCIES = ['axis']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Axis device event."""
add_devices([AxisBinarySensor(discovery_info['axis_event'], hass)], True)
class AxisBinarySensor(AxisDeviceEvent, BinarySensorDevice):
"""Representation of a binary Axis event."""
def __init__(self, axis_event, hass):
"""Initialize the binary sensor."""
self.hass = hass
self._state = False
self._delay = axis_event.device_config(CONF_TRIGGER_TIME)
self._timer = None
AxisDeviceEvent.__init__(self, axis_event)
@property
def is_on(self):
"""Return true if event is active."""
return self._state
def update(self):
"""Get the latest data and update the state."""
self._state = self.axis_event.is_tripped
def _update_callback(self):
"""Update the sensor's state, if needed."""
self.update()
if self._timer is not None:
self._timer()
self._timer = None
if self._delay > 0 and not self.is_on:
# Set timer to wait until updating the state
def _delay_update(now):
"""Timer callback for sensor update."""
_LOGGER.debug("%s Called delayed (%s sec) update.",
self._name, self._delay)
self.schedule_update_ha_state()
self._timer = None
self._timer = track_point_in_utc_time(
self.hass, _delay_update,
utcnow() + timedelta(seconds=self._delay))
else:
self.schedule_update_ha_state()
@@ -0,0 +1,95 @@
"""
Support for the myStrom buttons.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.mystrom/
"""
import asyncio
import logging
from homeassistant.components.binary_sensor import (BinarySensorDevice, DOMAIN)
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http']
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up myStrom Binary Sensor."""
hass.http.register_view(MyStromView(async_add_devices))
return True
class MyStromView(HomeAssistantView):
"""View to handle requests from myStrom buttons."""
url = '/api/mystrom'
name = 'api:mystrom'
def __init__(self, add_devices):
"""Initialize the myStrom URL endpoint."""
self.buttons = {}
self.add_devices = add_devices
@asyncio.coroutine
def get(self, request):
"""The GET request received from a myStrom button."""
res = yield from self._handle(request.app['hass'], request.GET)
return res
@asyncio.coroutine
def _handle(self, hass, data):
"""Handle requests to the myStrom endpoint."""
button_action = list(data.keys())[0]
button_id = data[button_action]
entity_id = '{}.{}_{}'.format(DOMAIN, button_id, button_action)
if button_action not in ['single', 'double', 'long', 'touch']:
_LOGGER.error(
"Received unidentified message from myStrom button: %s", data)
return ("Received unidentified message: {}".format(data),
HTTP_UNPROCESSABLE_ENTITY)
if entity_id not in self.buttons:
_LOGGER.info("New myStrom button/action detected: %s/%s",
button_id, button_action)
self.buttons[entity_id] = MyStromBinarySensor(
'{}_{}'.format(button_id, button_action))
hass.async_add_job(self.add_devices, [self.buttons[entity_id]])
else:
new_state = True if self.buttons[entity_id].state == 'off' \
else False
self.buttons[entity_id].async_on_update(new_state)
class MyStromBinarySensor(BinarySensorDevice):
"""Representation of a myStrom button."""
def __init__(self, button_id):
"""Initialize the myStrom Binary sensor."""
self._button_id = button_id
self._state = None
@property
def name(self):
"""Return the name of the sensor."""
return self._button_id
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self._state
def async_on_update(self, value):
"""Receive an update."""
self._state = value
self.hass.async_add_job(self.async_update_ha_state())
@@ -0,0 +1,131 @@
"""
Configure a binary_sensor using a digital input from a raspihats board.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.raspihats/
"""
import logging
import voluptuous as vol
from homeassistant.const import (
CONF_NAME, CONF_DEVICE_CLASS, DEVICE_DEFAULT_NAME
)
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
PLATFORM_SCHEMA, BinarySensorDevice
)
from homeassistant.components.raspihats import (
CONF_I2C_HATS, CONF_BOARD, CONF_ADDRESS, CONF_CHANNELS, CONF_INDEX,
CONF_INVERT_LOGIC, I2C_HAT_NAMES, I2C_HATS_MANAGER, I2CHatsException
)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['raspihats']
DEFAULT_INVERT_LOGIC = False
DEFAULT_DEVICE_CLASS = None
_CHANNELS_SCHEMA = vol.Schema([{
vol.Required(CONF_INDEX): cv.positive_int,
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean,
vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): cv.string,
}])
_I2C_HATS_SCHEMA = vol.Schema([{
vol.Required(CONF_BOARD): vol.In(I2C_HAT_NAMES),
vol.Required(CONF_ADDRESS): vol.Coerce(int),
vol.Required(CONF_CHANNELS): _CHANNELS_SCHEMA
}])
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_I2C_HATS): _I2C_HATS_SCHEMA,
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the raspihats binary_sensor devices."""
I2CHatBinarySensor.I2C_HATS_MANAGER = hass.data[I2C_HATS_MANAGER]
binary_sensors = []
i2c_hat_configs = config.get(CONF_I2C_HATS)
for i2c_hat_config in i2c_hat_configs:
address = i2c_hat_config[CONF_ADDRESS]
board = i2c_hat_config[CONF_BOARD]
try:
I2CHatBinarySensor.I2C_HATS_MANAGER.register_board(board, address)
for channel_config in i2c_hat_config[CONF_CHANNELS]:
binary_sensors.append(
I2CHatBinarySensor(
address,
channel_config[CONF_INDEX],
channel_config[CONF_NAME],
channel_config[CONF_INVERT_LOGIC],
channel_config[CONF_DEVICE_CLASS]
)
)
except I2CHatsException as ex:
_LOGGER.error(
"Failed to register " + board + "I2CHat@" + hex(address) + " "
+ str(ex)
)
add_devices(binary_sensors)
class I2CHatBinarySensor(BinarySensorDevice):
"""Represents a binary sensor that uses a I2C-HAT digital input."""
I2C_HATS_MANAGER = None
def __init__(self, address, channel, name, invert_logic, device_class):
"""Initialize sensor."""
self._address = address
self._channel = channel
self._name = name or DEVICE_DEFAULT_NAME
self._invert_logic = invert_logic
self._device_class = device_class
self._state = self.I2C_HATS_MANAGER.read_di(
self._address,
self._channel
)
def online_callback():
"""Callback fired when board is online."""
self.schedule_update_ha_state()
self.I2C_HATS_MANAGER.register_online_callback(
self._address,
self._channel,
online_callback
)
def edge_callback(state):
"""Read digital input state."""
self._state = state
self.schedule_update_ha_state()
self.I2C_HATS_MANAGER.register_di_callback(
self._address,
self._channel,
edge_callback
)
@property
def device_class(self):
"""Return the class of this sensor."""
return self._device_class
@property
def name(self):
"""Return the name of this sensor."""
return self._name
@property
def should_poll(self):
"""Polling not needed for this sensor."""
return False
@property
def is_on(self):
"""Return the state of this sensor."""
return self._state != self._invert_logic
@@ -0,0 +1,93 @@
"""
Support for binary sensor using the PiFace Digital I/O module on a RPi.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.rpi_pfio/
"""
import logging
import voluptuous as vol
import homeassistant.components.rpi_pfio as rpi_pfio
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import DEVICE_DEFAULT_NAME
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
ATTR_NAME = 'name'
ATTR_INVERT_LOGIC = 'invert_logic'
ATTR_SETTLE_TIME = 'settle_time'
CONF_PORTS = 'ports'
DEFAULT_INVERT_LOGIC = False
DEFAULT_SETTLE_TIME = 20
DEPENDENCIES = ['rpi_pfio']
PORT_SCHEMA = vol.Schema({
vol.Optional(ATTR_NAME, default=None): cv.string,
vol.Optional(ATTR_SETTLE_TIME, default=DEFAULT_SETTLE_TIME):
cv.positive_int,
vol.Optional(ATTR_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PORTS, default={}): vol.Schema({
cv.positive_int: PORT_SCHEMA
})
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the PiFace Digital Input devices."""
binary_sensors = []
ports = config.get('ports')
for port, port_entity in ports.items():
name = port_entity[ATTR_NAME]
settle_time = port_entity[ATTR_SETTLE_TIME] / 1000
invert_logic = port_entity[ATTR_INVERT_LOGIC]
binary_sensors.append(RPiPFIOBinarySensor(
hass, port, name, settle_time, invert_logic))
add_devices(binary_sensors, True)
rpi_pfio.activate_listener(hass)
class RPiPFIOBinarySensor(BinarySensorDevice):
"""Represent a binary sensor that a PiFace Digital Input."""
def __init__(self, hass, port, name, settle_time, invert_logic):
"""Initialize the RPi binary sensor."""
self._port = port
self._name = name or DEVICE_DEFAULT_NAME
self._invert_logic = invert_logic
self._state = None
def read_pfio(port):
"""Read state from PFIO."""
self._state = rpi_pfio.read_input(self._port)
self.schedule_update_ha_state()
rpi_pfio.edge_detect(hass, self._port, read_pfio, settle_time)
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def is_on(self):
"""Return the state of the entity."""
return self._state != self._invert_logic
def update(self):
"""Update the PFIO state."""
self._state = rpi_pfio.read_input(self._port)
@@ -4,6 +4,7 @@ Support for Wink binary sensors.
For more details about this platform, please refer to the documentation at
at https://home-assistant.io/components/binary_sensor.wink/
"""
import asyncio
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
@@ -101,6 +102,11 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
else:
self.capability = None
@asyncio.coroutine
def async_added_to_hass(self):
"""Callback when entity is added to hass."""
self.hass.data[DOMAIN]['entities']['binary_sensor'].append(self)
@property
def is_on(self):
"""Return true if the binary sensor is on."""
+1 -1
View File
@@ -13,7 +13,7 @@ from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, ATTR_FRIENDLY_NAME, ATTR_ARMED)
from homeassistant.helpers import discovery
REQUIREMENTS = ['blinkpy==0.5.2']
REQUIREMENTS = ['blinkpy==0.6.0']
_LOGGER = logging.getLogger(__name__)
+1 -1
View File
@@ -269,7 +269,7 @@ class CameraImageView(CameraView):
image = yield from camera.async_camera_image()
if image:
return web.Response(body=image)
return web.Response(body=image, content_type='image/jpeg')
return web.Response(status=500)
+31 -11
View File
@@ -12,18 +12,22 @@ import voluptuous as vol
import homeassistant.loader as loader
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_aiohttp_proxy_web)
async_get_clientsession, async_aiohttp_proxy_web,
async_aiohttp_proxy_stream)
REQUIREMENTS = ['amcrest==1.1.9']
REQUIREMENTS = ['amcrest==1.2.0']
DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__)
CONF_RESOLUTION = 'resolution'
CONF_STREAM_SOURCE = 'stream_source'
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
DEFAULT_NAME = 'Amcrest Camera'
DEFAULT_PORT = 80
@@ -40,7 +44,8 @@ RESOLUTION_LIST = {
STREAM_SOURCE_LIST = {
'mjpeg': 0,
'snapshot': 1
'snapshot': 1,
'rtsp': 2,
}
CONTENT_TYPE_HEADER = 'Content-Type'
@@ -56,6 +61,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
vol.All(vol.In(STREAM_SOURCE_LIST)),
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
})
@@ -92,8 +98,9 @@ class AmcrestCam(Camera):
super(AmcrestCam, self).__init__()
self._camera = camera
self._base_url = self._camera.get_base_url()
self._hass = hass
self._name = device_info.get(CONF_NAME)
self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)]
self._stream_source = STREAM_SOURCE_LIST[
device_info.get(CONF_STREAM_SOURCE)
@@ -117,15 +124,28 @@ class AmcrestCam(Camera):
yield from super().handle_async_mjpeg_stream(request)
return
# Otherwise, stream an MJPEG image stream directly from the camera
websession = async_get_clientsession(self.hass)
streaming_url = '{0}mjpg/video.cgi?channel=0&subtype={1}'.format(
self._base_url, self._resolution)
elif self._stream_source == STREAM_SOURCE_LIST['mjpeg']:
# stream an MJPEG image stream directly from the camera
websession = async_get_clientsession(self.hass)
streaming_url = self._camera.mjpeg_url(typeno=self._resolution)
stream_coro = websession.get(
streaming_url, auth=self._token, timeout=TIMEOUT)
stream_coro = websession.get(
streaming_url, auth=self._token, timeout=TIMEOUT)
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
else:
# streaming via fmpeg
from haffmpeg import CameraMjpeg
streaming_url = self._camera.rtsp_url(typeno=self._resolution)
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
yield from stream.open_camera(
streaming_url, extra_cmd=self._ffmpeg_arguments)
yield from async_aiohttp_proxy_stream(
self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver')
yield from stream.close()
@property
def name(self):
+38
View File
@@ -0,0 +1,38 @@
"""
Support for Axis camera streaming.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.axis/
"""
import logging
from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.components.camera.mjpeg import (
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['axis']
DOMAIN = 'axis'
def _get_image_url(host, mode):
if mode == 'mjpeg':
return 'http://{}/axis-cgi/mjpg/video.cgi'.format(host)
elif mode == 'single':
return 'http://{}/axis-cgi/jpg/image.cgi'.format(host)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Axis camera."""
device_info = {
CONF_NAME: discovery_info['name'],
CONF_USERNAME: discovery_info['username'],
CONF_PASSWORD: discovery_info['password'],
CONF_MJPEG_URL: _get_image_url(discovery_info['host'], 'mjpeg'),
CONF_STILL_IMAGE_URL: _get_image_url(discovery_info['host'], 'single'),
CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION,
}
add_devices([MjpegCamera(hass, device_info)])
+9 -11
View File
@@ -20,12 +20,15 @@ _LOGGER = logging.getLogger(__name__)
CONF_NVR = 'nvr'
CONF_KEY = 'key'
CONF_PASSWORD = 'password'
DEFAULT_PASSWORD = 'ubnt'
DEFAULT_PORT = 7080
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NVR): cv.string,
vol.Required(CONF_KEY): cv.string,
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
@@ -34,6 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Discover cameras on a Unifi NVR."""
addr = config[CONF_NVR]
key = config[CONF_KEY]
password = config[CONF_PASSWORD]
port = config[CONF_PORT]
from uvcclient import nvr
@@ -59,7 +63,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([UnifiVideoCamera(nvrconn,
camera[identifier],
camera['name'])
camera['name'],
password)
for camera in cameras])
return True
@@ -67,12 +72,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class UnifiVideoCamera(Camera):
"""A Ubiquiti Unifi Video Camera."""
def __init__(self, nvr, uuid, name):
def __init__(self, nvr, uuid, name, password):
"""Initialize an Unifi camera."""
super(UnifiVideoCamera, self).__init__()
self._nvr = nvr
self._uuid = uuid
self._name = name
self._password = password
self.is_streaming = False
self._connect_addr = None
self._camera = None
@@ -102,7 +108,6 @@ class UnifiVideoCamera(Camera):
def _login(self):
"""Login to the camera."""
from uvcclient import camera as uvc_camera
from uvcclient import store as uvc_store
caminfo = self._nvr.get_camera(self._uuid)
if self._connect_addr:
@@ -110,13 +115,6 @@ class UnifiVideoCamera(Camera):
else:
addrs = [caminfo['host'], caminfo['internalHost']]
store = uvc_store.get_info_store()
password = store.get_camera_password(self._uuid)
if password is None:
_LOGGER.debug("Logging into camera %(name)s with default password",
dict(name=self._name))
password = 'ubnt'
if self._nvr.server_version >= (3, 2, 0):
client_cls = uvc_camera.UVCCameraClientV320
else:
@@ -126,7 +124,7 @@ class UnifiVideoCamera(Camera):
for addr in addrs:
try:
camera = client_cls(
addr, caminfo['username'], password)
addr, caminfo['username'], self._password)
camera.login()
_LOGGER.debug("Logged into UVC camera %(name)s via %(addr)s",
dict(name=self._name, addr=addr))
@@ -107,12 +107,7 @@ class ZoneMinderCamera(MjpegCamera):
self._monitor_id)
return
if not status_response.get("success", False):
_LOGGER.warning("Alarm status API call failed for monitor %i",
self._monitor_id)
return
self._is_recording = status_response['status'] == ZM_STATE_ALARM
self._is_recording = status_response.get('status') == ZM_STATE_ALARM
@property
def is_recording(self):
+4 -4
View File
@@ -149,22 +149,22 @@ class SensiboClimate(ClimateDevice):
@property
def current_fan_mode(self):
"""Return the fan setting."""
return self._ac_states['fanLevel']
return self._ac_states.get('fanLevel')
@property
def fan_list(self):
"""List of available fan modes."""
return self._current_capabilities['fanLevels']
return self._current_capabilities.get('fanLevels')
@property
def current_swing_mode(self):
"""Return the fan setting."""
return self._ac_states['swing']
return self._ac_states.get('swing')
@property
def swing_list(self):
"""List of available swing modes."""
return self._current_capabilities['swing']
return self._current_capabilities.get('swing')
@property
def name(self):
+7
View File
@@ -4,6 +4,8 @@ Support for Wink thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.wink/
"""
import asyncio
from homeassistant.components.wink import WinkDevice, DOMAIN
from homeassistant.components.climate import (
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
@@ -52,6 +54,11 @@ class WinkThermostat(WinkDevice, ClimateDevice):
super().__init__(wink, hass)
self._config_temp_unit = temp_unit
@asyncio.coroutine
def async_added_to_hass(self):
"""Callback when entity is added to hass."""
self.hass.data[DOMAIN]['entities']['climate'].append(self)
@property
def temperature_unit(self):
"""Return the unit of measurement."""
+75 -13
View File
@@ -5,7 +5,7 @@ import os
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import EVENT_COMPONENT_LOADED
from homeassistant.const import EVENT_COMPONENT_LOADED, CONF_ID
from homeassistant.setup import (
async_prepare_setup_platform, ATTR_COMPONENT)
from homeassistant.components.frontend import register_built_in_panel
@@ -14,8 +14,8 @@ from homeassistant.util.yaml import load_yaml, dump
DOMAIN = 'config'
DEPENDENCIES = ['http']
SECTIONS = ('core', 'group', 'hassbian')
ON_DEMAND = ('zwave', )
SECTIONS = ('core', 'group', 'hassbian', 'automation')
ON_DEMAND = ('zwave')
@asyncio.coroutine
@@ -60,7 +60,7 @@ def async_setup(hass, config):
return True
class EditKeyBasedConfigView(HomeAssistantView):
class BaseEditConfigView(HomeAssistantView):
"""Configure a Group endpoint."""
def __init__(self, component, config_type, path, key_schema, data_schema,
@@ -73,13 +73,29 @@ class EditKeyBasedConfigView(HomeAssistantView):
self.data_schema = data_schema
self.post_write_hook = post_write_hook
def _empty_config(self):
"""Empty config if file not found."""
raise NotImplementedError
def _get_value(self, data, config_key):
"""Get value."""
raise NotImplementedError
def _write_value(self, data, config_key, new_value):
"""Set value."""
raise NotImplementedError
@asyncio.coroutine
def get(self, request, config_key):
"""Fetch device specific config."""
hass = request.app['hass']
current = yield from hass.loop.run_in_executor(
None, _read, hass.config.path(self.path))
return self.json(current.get(config_key, {}))
current = yield from self.read_config(hass)
value = self._get_value(current, config_key)
if value is None:
return self.json_message('Resource not found', 404)
return self.json(value)
@asyncio.coroutine
def post(self, request, config_key):
@@ -104,10 +120,10 @@ class EditKeyBasedConfigView(HomeAssistantView):
hass = request.app['hass']
path = hass.config.path(self.path)
current = yield from hass.loop.run_in_executor(None, _read, path)
current.setdefault(config_key, {}).update(data)
current = yield from self.read_config(hass)
self._write_value(current, config_key, data)
yield from hass.loop.run_in_executor(None, _write, path, current)
yield from hass.async_add_job(_write, path, current)
if self.post_write_hook is not None:
hass.async_add_job(self.post_write_hook(hass))
@@ -116,13 +132,59 @@ class EditKeyBasedConfigView(HomeAssistantView):
'result': 'ok',
})
@asyncio.coroutine
def read_config(self, hass):
"""Read the config."""
current = yield from hass.async_add_job(
_read, hass.config.path(self.path))
if not current:
current = self._empty_config()
return current
class EditKeyBasedConfigView(BaseEditConfigView):
"""Configure a list of entries."""
def _empty_config(self):
"""Return an empty config."""
return {}
def _get_value(self, data, config_key):
"""Get value."""
return data.get(config_key, {})
def _write_value(self, data, config_key, new_value):
"""Set value."""
data.setdefault(config_key, {}).update(new_value)
class EditIdBasedConfigView(BaseEditConfigView):
"""Configure key based config entries."""
def _empty_config(self):
"""Return an empty config."""
return []
def _get_value(self, data, config_key):
"""Get value."""
return next(
(val for val in data if val.get(CONF_ID) == config_key), None)
def _write_value(self, data, config_key, new_value):
"""Set value."""
value = self._get_value(data, config_key)
if value is None:
value = {CONF_ID: config_key}
data.append(value)
value.update(new_value)
def _read(path):
"""Read YAML helper."""
if not os.path.isfile(path):
with open(path, 'w'):
pass
return {}
return None
return load_yaml(path)
@@ -0,0 +1,20 @@
"""Provide configuration end points for Z-Wave."""
import asyncio
from homeassistant.components.config import EditIdBasedConfigView
from homeassistant.components.automation import (
PLATFORM_SCHEMA, DOMAIN, async_reload)
import homeassistant.helpers.config_validation as cv
CONFIG_PATH = 'automations.yaml'
@asyncio.coroutine
def async_setup(hass):
"""Set up the Automation config API."""
hass.http.register_view(EditIdBasedConfigView(
DOMAIN, 'config', CONFIG_PATH, cv.string,
PLATFORM_SCHEMA, post_write_hook=async_reload
))
return True
+8 -3
View File
@@ -9,9 +9,11 @@ the user has submitted configuration information.
import asyncio
import logging
from homeassistant.core import callback as async_callback
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
ATTR_ENTITY_PICTURE
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.util.async import run_callback_threadsafe
_LOGGER = logging.getLogger(__name__)
_REQUESTS = {}
@@ -43,7 +45,9 @@ def request_config(
Will return an ID to be used for sequent calls.
"""
instance = _get_instance(hass)
instance = run_callback_threadsafe(hass.loop,
_async_get_instance,
hass).result()
request_id = instance.request_config(
name, callback,
@@ -79,7 +83,8 @@ def async_setup(hass, config):
return True
def _get_instance(hass):
@async_callback
def _async_get_instance(hass):
"""Get an instance per hass object."""
instance = hass.data.get(_KEY_INSTANCE)
@@ -97,7 +102,7 @@ class Configurator(object):
self.hass = hass
self._cur_id = 0
self._requests = {}
hass.services.register(
hass.services.async_register(
DOMAIN, SERVICE_CONFIGURE, self.handle_service_call)
def request_config(
@@ -0,0 +1,62 @@
"""
Support for Lutron Caseta SerenaRollerShade.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.lutron_caseta/
"""
import logging
from homeassistant.components.cover import (
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE)
from homeassistant.components.lutron_caseta import (
LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['lutron_caseta']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Lutron Caseta Serena shades as a cover device."""
devs = []
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
cover_devices = bridge.get_devices_by_types(["SerenaRollerShade"])
for cover_device in cover_devices:
dev = LutronCasetaCover(cover_device, bridge)
devs.append(dev)
add_devices(devs, True)
class LutronCasetaCover(LutronCasetaDevice, CoverDevice):
"""Representation of a Lutron Serena shade."""
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_OPEN | SUPPORT_CLOSE
@property
def is_closed(self):
"""Return if the cover is closed."""
return self._state["current_state"] < 1
def close_cover(self):
"""Close the cover."""
self._smartbridge.set_value(self._device_id, 0)
def open_cover(self):
"""Open the cover."""
self._smartbridge.set_value(self._device_id, 100)
def set_cover_position(self, position, **kwargs):
"""Move the roller shutter to a specific position."""
self._smartbridge.set_value(self._device_id, position)
def update(self):
"""Call when forcing a refresh of the device."""
self._state = self._smartbridge.get_device_by_id(self._device_id)
_LOGGER.debug(self._state)
+7
View File
@@ -4,6 +4,8 @@ Support for Wink Covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.wink/
"""
import asyncio
from homeassistant.components.cover import CoverDevice
from homeassistant.components.wink import WinkDevice, DOMAIN
@@ -31,6 +33,11 @@ class WinkCoverDevice(WinkDevice, CoverDevice):
"""Initialize the cover."""
super().__init__(wink, hass)
@asyncio.coroutine
def async_added_to_hass(self):
"""Callback when entity is added to hass."""
self.hass.data[DOMAIN]['entities']['cover'].append(self)
def close_cover(self):
"""Close the shade."""
self.wink.set_state(0)
+120
View File
@@ -0,0 +1,120 @@
"""
A component which allows you to send data to Datadog.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/datadog/
"""
import logging
import voluptuous as vol
from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_PREFIX,
EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED,
STATE_UNKNOWN)
from homeassistant.helpers import state as state_helper
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['datadog==0.15.0']
_LOGGER = logging.getLogger(__name__)
CONF_RATE = 'rate'
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 8125
DEFAULT_PREFIX = 'hass'
DEFAULT_RATE = 1
DOMAIN = 'datadog'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): cv.string,
vol.Optional(CONF_RATE, default=DEFAULT_RATE):
vol.All(vol.Coerce(int), vol.Range(min=1)),
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Setup the Datadog component."""
from datadog import initialize, statsd
conf = config[DOMAIN]
host = conf.get(CONF_HOST)
port = conf.get(CONF_PORT)
sample_rate = conf.get(CONF_RATE)
prefix = conf.get(CONF_PREFIX)
initialize(statsd_host=host, statsd_port=port)
def logbook_entry_listener(event):
"""Listen for logbook entries and send them as events."""
name = event.data.get('name')
message = event.data.get('message')
statsd.event(
title="Home Assistant",
text="%%% \n **{}** {} \n %%%".format(name, message),
tags=[
"entity:{}".format(event.data.get('entity_id')),
"domain:{}".format(event.data.get('domain'))
]
)
_LOGGER.debug('Sent event %s', event.data.get('entity_id'))
def state_changed_listener(event):
"""Listen for new messages on the bus and sends them to Datadog."""
state = event.data.get('new_state')
if state is None or state.state == STATE_UNKNOWN:
return
if state.attributes.get('hidden') is True:
return
states = dict(state.attributes)
metric = "{}.{}".format(prefix, state.domain)
tags = ["entity:{}".format(state.entity_id)]
for key, value in states.items():
if isinstance(value, (float, int)):
attribute = "{}.{}".format(metric, key.replace(' ', '_'))
statsd.gauge(
attribute,
value,
sample_rate=sample_rate,
tags=tags
)
_LOGGER.debug(
'Sent metric %s: %s (tags: %s)',
attribute,
value,
tags
)
try:
value = state_helper.state_as_number(state)
except ValueError:
_LOGGER.debug(
'Error sending %s: %s (tags: %s)',
metric,
state.state,
tags
)
return
statsd.gauge(
metric,
value,
sample_rate=sample_rate,
tags=tags
)
_LOGGER.debug('Sent metric %s: %s (tags: %s)', metric, value, tags)
hass.bus.listen(EVENT_LOGBOOK_ENTRY, logbook_entry_listener)
hass.bus.listen(EVENT_STATE_CHANGED, state_changed_listener)
return True
+8 -8
View File
@@ -157,28 +157,28 @@ def async_setup(hass, config):
}},
]}))
tasks2.append(group.Group.async_create_group(hass, 'living room', [
tasks2.append(group.Group.async_create_group(hass, 'Living Room', [
lights[1], switches[0], 'input_select.living_room_preset',
'cover.living_room_window', media_players[1],
'scene.romantic_lights']))
tasks2.append(group.Group.async_create_group(hass, 'bedroom', [
tasks2.append(group.Group.async_create_group(hass, 'Bedroom', [
lights[0], switches[1], media_players[0],
'input_slider.noise_allowance']))
tasks2.append(group.Group.async_create_group(hass, 'kitchen', [
tasks2.append(group.Group.async_create_group(hass, 'Kitchen', [
lights[2], 'cover.kitchen_window', 'lock.kitchen_door']))
tasks2.append(group.Group.async_create_group(hass, 'doors', [
tasks2.append(group.Group.async_create_group(hass, 'Doors', [
'lock.front_door', 'lock.kitchen_door',
'garage_door.right_garage_door', 'garage_door.left_garage_door']))
tasks2.append(group.Group.async_create_group(hass, 'automations', [
tasks2.append(group.Group.async_create_group(hass, 'Automations', [
'input_select.who_cooks', 'input_boolean.notify', ]))
tasks2.append(group.Group.async_create_group(hass, 'people', [
tasks2.append(group.Group.async_create_group(hass, 'People', [
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
'device_tracker.demo_paulus']))
tasks2.append(group.Group.async_create_group(hass, 'downstairs', [
tasks2.append(group.Group.async_create_group(hass, 'Downstairs', [
'group.living_room', 'group.kitchen',
'scene.romantic_lights', 'cover.kitchen_window',
'cover.living_room_window', 'group.doors',
'thermostat.ecobee',
'climate.ecobee',
], view=True))
results = yield from asyncio.gather(*tasks2, loop=hass.loop)
@@ -14,12 +14,13 @@ from homeassistant.core import callback
import homeassistant.util.dt as dt_util
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
from homeassistant.helpers.event import (
async_track_point_in_time, async_track_state_change)
async_track_point_in_utc_time, async_track_state_change)
from homeassistant.helpers.sun import is_up, get_astral_event_next
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv
DOMAIN = 'device_sun_light_trigger'
DEPENDENCIES = ['light', 'device_tracker', 'group', 'sun']
DEPENDENCIES = ['light', 'device_tracker', 'group']
CONF_DEVICE_GROUP = 'device_group'
CONF_DISABLE_TURN_OFF = 'disable_turn_off'
@@ -50,7 +51,6 @@ def async_setup(hass, config):
device_tracker = get_component('device_tracker')
group = get_component('group')
light = get_component('light')
sun = get_component('sun')
conf = config[DOMAIN]
disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF)
light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS)
@@ -78,7 +78,7 @@ def async_setup(hass, config):
Async friendly.
"""
next_setting = sun.next_setting(hass)
next_setting = get_astral_event_next(hass, 'sunset')
if not next_setting:
return None
return next_setting - LIGHT_TRANSITION_TIME * len(light_ids)
@@ -103,7 +103,7 @@ def async_setup(hass, config):
# Track every time sun rises so we can schedule a time-based
# pre-sun set event
@callback
def schedule_light_turn_on(entity, old_state, new_state):
def schedule_light_turn_on(now):
"""Turn on all the lights at the moment sun sets.
We will schedule to have each light start after one another
@@ -114,26 +114,26 @@ def async_setup(hass, config):
return
for index, light_id in enumerate(light_ids):
async_track_point_in_time(
async_track_point_in_utc_time(
hass, async_turn_on_factory(light_id),
start_point + index * LIGHT_TRANSITION_TIME)
async_track_state_change(hass, sun.ENTITY_ID, schedule_light_turn_on,
sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON)
async_track_point_in_utc_time(hass, schedule_light_turn_on,
get_astral_event_next(hass, 'sunrise'))
# If the sun is already above horizon schedule the time-based pre-sun set
# event.
if sun.is_on(hass):
schedule_light_turn_on(None, None, None)
if is_up(hass):
schedule_light_turn_on(None)
@callback
def check_light_on_dev_state_change(entity, old_state, new_state):
"""Handle tracked device state changes."""
lights_are_on = group.is_on(hass, light_group)
light_needed = not (lights_are_on or sun.is_on(hass))
light_needed = not (lights_are_on or is_up(hass))
# These variables are needed for the elif check
now = dt_util.now()
now = dt_util.utcnow()
start_point = calc_time_for_light_when_sunset()
# Do we need lights?
@@ -146,7 +146,7 @@ def async_setup(hass, config):
# Check this by seeing if current time is later then the point
# in time when we would start putting the lights on.
elif (start_point and
start_point < now < sun.next_setting(hass)):
start_point < now < get_astral_event_next(hass, 'sunset')):
# Check for every light if it would be on if someone was home
# when the fading in started and turn it on if so
@@ -21,7 +21,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
REQUIREMENTS = ['aioautomatic==0.3.1']
REQUIREMENTS = ['aioautomatic==0.4.0']
_LOGGER = logging.getLogger(__name__)
@@ -31,7 +31,8 @@ CONF_DEVICES = 'devices'
DEFAULT_TIMEOUT = 5
SCOPE = ['location', 'vehicle:profile', 'trip']
DEFAULT_SCOPE = ['location', 'vehicle:profile', 'trip']
FULL_SCOPE = DEFAULT_SCOPE + ['current_location']
ATTR_FUEL_LEVEL = 'fuel_level'
@@ -58,8 +59,17 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
client_session=async_get_clientsession(hass),
request_kwargs={'timeout': DEFAULT_TIMEOUT})
try:
session = yield from client.create_session_from_password(
SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD])
try:
session = yield from client.create_session_from_password(
FULL_SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD])
except aioautomatic.exceptions.ForbiddenError as exc:
if not str(exc).startswith("invalid_scope"):
raise exc
_LOGGER.info("Client not authorized for current_location scope. "
"location:updated events will not be received.")
session = yield from client.create_session_from_password(
DEFAULT_SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD])
data = AutomaticData(
hass, client, session, config[CONF_DEVICES], async_see)
@@ -22,7 +22,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
INTERFACES = 2
DEFAULT_TIMEOUT = 10
REQUIREMENTS = ['beautifulsoup4==4.5.3']
REQUIREMENTS = ['beautifulsoup4==4.6.0']
_LOGGER = logging.getLogger(__name__)
@@ -163,6 +163,7 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
self._log_out()
return self.last_results.keys()
# pylint: disable=no-self-use
@@ -195,8 +196,9 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
self.sysauth = regex_result.group(1)
_LOGGER.info(self.sysauth)
return True
except ValueError:
_LOGGER.error("Couldn't fetch auth tokens!")
except (ValueError, KeyError) as _:
_LOGGER.error("Couldn't fetch auth tokens! Response was: %s",
response.text)
return False
@Throttle(MIN_TIME_BETWEEN_SCANS)
@@ -250,6 +252,21 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
return False
def _log_out(self):
with self.lock:
_LOGGER.info("Logging out of router admin interface...")
url = ('http://{}/cgi-bin/luci/;stok={}/admin/system?'
'form=logout').format(self.host, self.stok)
referer = 'http://{}/webpages/index.html'.format(self.host)
requests.post(url,
params={'operation': 'write'},
headers={'referer': referer},
cookies={'sysauth': self.sysauth})
self.stok = ''
self.sysauth = ''
class Tplink4DeviceScanner(TplinkDeviceScanner):
"""This class queries an Archer C7 router with TP-Link firmware 150427."""
+3 -1
View File
@@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==1.0.0rc3']
REQUIREMENTS = ['netdisco==1.0.0']
DOMAIN = 'discovery'
@@ -31,6 +31,7 @@ SERVICE_WEMO = 'belkin_wemo'
SERVICE_HASS_IOS_APP = 'hass_ios'
SERVICE_IKEA_TRADFRI = 'ikea_tradfri'
SERVICE_HASSIO = 'hassio'
SERVICE_AXIS = 'axis'
SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None),
@@ -38,6 +39,7 @@ SERVICE_HANDLERS = {
SERVICE_WEMO: ('wemo', None),
SERVICE_IKEA_TRADFRI: ('tradfri', None),
SERVICE_HASSIO: ('hassio', None),
SERVICE_AXIS: ('axis', None),
'philips_hue': ('light', 'hue'),
'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'),
+2 -2
View File
@@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import state as state_helper
from homeassistant.util import Throttle
REQUIREMENTS = ['dweepy==0.2.0']
REQUIREMENTS = ['dweepy==0.3.0']
_LOGGER = logging.getLogger(__name__)
@@ -67,4 +67,4 @@ def send_data(name, msg):
try:
dweepy.dweet_for(name, msg)
except dweepy.DweepyError:
_LOGGER.error("Error saving data '%s' to Dweet.io", msg)
_LOGGER.error("Error saving data to Dweet.io: %s", msg)
+4 -1
View File
@@ -24,7 +24,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
REQUIREMENTS = ['pyeight==0.0.4']
REQUIREMENTS = ['pyeight==0.0.5']
_LOGGER = logging.getLogger(__name__)
@@ -145,6 +145,9 @@ def async_setup(hass, config):
sensors.append('{}_{}'.format(obj.side, sensor))
binary_sensors.append('{}_presence'.format(obj.side))
sensors.append('room_temp')
else:
# No users, cannot continue
return False
hass.async_add_job(discovery.async_load_platform(
hass, 'sensor', DOMAIN, {
+8
View File
@@ -4,6 +4,7 @@ Support for Wink fans.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/fan.wink/
"""
import asyncio
import logging
from homeassistant.components.fan import (FanEntity, SPEED_HIGH,
@@ -12,6 +13,8 @@ from homeassistant.components.fan import (FanEntity, SPEED_HIGH,
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.wink import WinkDevice, DOMAIN
DEPENDENCIES = ['wink']
_LOGGER = logging.getLogger(__name__)
SPEED_LOWEST = 'lowest'
@@ -34,6 +37,11 @@ class WinkFanDevice(WinkDevice, FanEntity):
"""Initialize the fan."""
super().__init__(wink, hass)
@asyncio.coroutine
def async_added_to_hass(self):
"""Callback when entity is added to hass."""
self.hass.data[DOMAIN]['entities']['fan'].append(self)
def set_direction(self: ToggleEntity, direction: str) -> None:
"""Set the direction of the fan."""
self.wink.set_fan_direction(direction)
+6 -5
View File
@@ -1,22 +1,23 @@
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
FINGERPRINTS = {
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
"core.js": "5d08475f03adb5969bd31855d5ca0cfd",
"frontend.html": "5999c8fac69c503b846672cae75a12b0",
"compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812",
"core.js": "d4a7cb8c80c62b536764e0e81385f6aa",
"frontend.html": "fbb9d6bdd3d661db26cad9475a5e22f1",
"mdi.html": "f407a5a57addbe93817ee1b244d33fbe",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-automation.html": "21cba0a4fee9d2b45dda47f7a1dd82d8",
"panels/ha-panel-config.html": "59d9eb28758b497a4d9b2428f978b9b1",
"panels/ha-panel-dev-event.html": "2db9c218065ef0f61d8d08db8093cad2",
"panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d",
"panels/ha-panel-dev-service.html": "415552027cb083badeff5f16080410ed",
"panels/ha-panel-dev-state.html": "d70314913b8923d750932367b1099750",
"panels/ha-panel-dev-template.html": "567fbf86735e1b891e40c2f4060fec9b",
"panels/ha-panel-hassio.html": "23d175b6744c20e2fdf475b6efdaa1d3",
"panels/ha-panel-hassio.html": "9474ba65077371622f21ed9a30cf5229",
"panels/ha-panel-history.html": "89062c48c76206cad1cec14ddbb1cbb1",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163",
"panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4",
"panels/ha-panel-zwave.html": "84fb45638d2a69bac343246a687f647c",
"panels/ha-panel-zwave.html": "19336d2c50c91dd6a122acc0606ff10d",
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
}
@@ -1 +1 @@
!(function(){"use strict";function e(e,r){var t=arguments;if(void 0===e||null===e)throw new TypeError("Cannot convert first argument to object");for(var n=Object(e),o=1;o<arguments.length;o++){var i=t[o];if(void 0!==i&&null!==i)for(var l=Object.keys(Object(i)),a=0,c=l.length;a<c;a++){var b=l[a],f=Object.getOwnPropertyDescriptor(i,b);void 0!==f&&f.enumerable&&(n[b]=i[b])}}return n}function r(){Object.assign||Object.defineProperty(Object,"assign",{enumerable:!1,configurable:!0,writable:!0,value:e})}var t={assign:e,polyfill:r};t.polyfill()})();
!function(){"use strict";function e(e,t){if(void 0===e||null===e)throw new TypeError("Cannot convert first argument to object");for(var r=Object(e),n=1;n<arguments.length;n++){var o=arguments[n];if(void 0!==o&&null!==o)for(var i=Object.keys(Object(o)),l=0,c=i.length;l<c;l++){var a=i[l],b=Object.getOwnPropertyDescriptor(o,a);void 0!==b&&b.enumerable&&(r[a]=o[a])}}return r}function t(){Object.assign||Object.defineProperty(Object,"assign",{enumerable:!1,configurable:!0,writable:!0,value:e})}({assign:e,polyfill:t}).polyfill()}();
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+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.25']
REQUIREMENTS = ['pyhomematic==0.1.26']
DOMAIN = 'homematic'
@@ -11,25 +11,26 @@ import os
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_NAME, CONF_ENTITY_ID)
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.loader import get_component
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'image_processing'
DEPENDENCIES = ['camera']
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=10)
DEVICE_CLASSES = [
'alpr', # automatic license plate recognition
'face', # face
'alpr', # Automatic license plate recognition
'face', # Face
'ocr', # OCR
]
SERVICE_SCAN = 'scan'
@@ -1,7 +1,7 @@
"""
Support for the demo image processing.
For more details about this component, please refer to the documentation at
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/demo/
"""
from homeassistant.components.image_processing import ATTR_CONFIDENCE
@@ -12,7 +12,7 @@ from homeassistant.components.image_processing.microsoft_face_identify import (
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the demo image_processing platform."""
"""Set up the demo image processing platform."""
add_devices([
DemoImageProcessingAlpr('camera.demo_camera', "Demo Alpr"),
DemoImageProcessingFace(
@@ -21,10 +21,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class DemoImageProcessingAlpr(ImageProcessingAlprEntity):
"""Demo alpr image processing entity."""
"""Demo ALPR image processing entity."""
def __init__(self, camera_entity, name):
"""Initialize demo alpr."""
"""Initialize demo ALPR image processing entity."""
super().__init__()
self._name = name
@@ -61,7 +61,7 @@ class DemoImageProcessingFace(ImageProcessingFaceEntity):
"""Demo face identify image processing entity."""
def __init__(self, camera_entity, name):
"""Initialize demo alpr."""
"""Initialize demo face image processing entity."""
super().__init__()
self._name = name
@@ -1,7 +1,7 @@
"""
Component that will help set the dlib face detect processing.
Component that will help set the Dlib face detect processing.
For more details about this component, please refer to the documentation at
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/image_processing.dlib_face_detect/
"""
import logging
@@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Microsoft Face detection platform."""
"""Set up the Dlib Face detection platform."""
entities = []
for camera in config[CONF_SOURCE]:
entities.append(DlibFaceDetectEntity(
@@ -35,7 +35,7 @@ class DlibFaceDetectEntity(ImageProcessingFaceEntity):
"""Dlib Face API entity for identify."""
def __init__(self, camera_entity, name=None):
"""Initialize Dlib."""
"""Initialize Dlib face entity."""
super().__init__()
self._camera = camera_entity
@@ -62,7 +62,7 @@ class DlibFaceDetectEntity(ImageProcessingFaceEntity):
import face_recognition
fak_file = io.BytesIO(image)
fak_file.name = "snapshot.jpg"
fak_file.name = 'snapshot.jpg'
fak_file.seek(0)
image = face_recognition.load_image_file(fak_file)
@@ -1,7 +1,7 @@
"""
Component that will help set the dlib face detect processing.
Component that will help set the Dlib face detect processing.
For more details about this component, please refer to the documentation at
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/image_processing.dlib_face_identify/
"""
import logging
@@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Microsoft Face detection platform."""
"""Set up the Dlib Face detection platform."""
entities = []
for camera in config[CONF_SOURCE]:
entities.append(DlibFaceIdentifyEntity(
@@ -43,7 +43,7 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity):
"""Dlib Face API entity for identify."""
def __init__(self, camera_entity, faces, name=None):
"""Initialize Dlib."""
"""Initialize Dlib face identify entry."""
# pylint: disable=import-error
import face_recognition
super().__init__()
@@ -77,7 +77,7 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity):
import face_recognition
fak_file = io.BytesIO(image)
fak_file.name = "snapshot.jpg"
fak_file.name = 'snapshot.jpg'
fak_file.seek(0)
image = face_recognition.load_image_file(fak_file)
@@ -1,7 +1,7 @@
"""
Component that will help set the microsoft face detect processing.
Component that will help set the Microsoft face detect processing.
For more details about this component, please refer to the documentation at
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/image_processing.microsoft_face_detect/
"""
import asyncio
@@ -1,5 +1,5 @@
"""
Component that will help set the microsoft face for verify processing.
Component that will help set the Microsoft face for verify processing.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/image_processing.microsoft_face_identify/
@@ -62,8 +62,8 @@ class ImageProcessingFaceEntity(ImageProcessingEntity):
def __init__(self):
"""Initialize base face identify/verify entity."""
self.faces = [] # last scan data
self.total_faces = 0 # face count
self.faces = []
self.total_faces = 0
@property
def state(self):
@@ -71,11 +71,11 @@ class ImageProcessingFaceEntity(ImageProcessingEntity):
confidence = 0
state = STATE_UNKNOWN
# no confidence support
# No confidence support
if not self.confidence:
return self.total_faces
# search high confidence
# Search high confidence
for face in self.faces:
if ATTR_CONFIDENCE not in face:
continue
@@ -128,7 +128,7 @@ class ImageProcessingFaceEntity(ImageProcessingEntity):
This method must be run in the event loop.
"""
# send events
# Send events
for face in faces:
if ATTR_CONFIDENCE in face and self.confidence:
if face[ATTR_CONFIDENCE] < self.confidence:
@@ -139,7 +139,7 @@ class ImageProcessingFaceEntity(ImageProcessingEntity):
self.hass.bus.async_fire, EVENT_DETECT_FACE, face
)
# update entity store
# Update entity store
self.faces = faces
self.total_faces = total
@@ -200,7 +200,7 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity):
_LOGGER.error("Can't process image on Microsoft face: %s", err)
return
# parse data
# Parse data
knwon_faces = []
total = 0
for face in detect:
@@ -220,5 +220,4 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity):
ATTR_CONFIDENCE: data['confidence'] * 100,
})
# process data
self.async_process_faces(knwon_faces, total)
@@ -1,17 +1,18 @@
"""
Component that will help set the OpenALPR cloud for ALPR processing.
For more details about this component, please refer to the documentation at
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/image_processing.openalpr_cloud/
"""
import asyncio
from base64 import b64encode
import logging
from base64 import b64encode
import aiohttp
import async_timeout
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.core import split_entity_id
from homeassistant.const import CONF_API_KEY
from homeassistant.components.image_processing import (
@@ -19,7 +20,6 @@ from homeassistant.components.image_processing import (
from homeassistant.components.image_processing.openalpr_local import (
ImageProcessingAlprEntity)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -44,8 +44,7 @@ CONF_REGION = 'region'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_REGION):
vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)),
vol.Required(CONF_REGION): vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)),
})
@@ -70,7 +69,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class OpenAlprCloudEntity(ImageProcessingAlprEntity):
"""OpenALPR cloud entity."""
"""Representation of an OpenALPR cloud entity."""
def __init__(self, camera_entity, params, confidence, name=None):
"""Initialize OpenALPR cloud API."""
@@ -129,7 +128,7 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity):
_LOGGER.error("Timeout for OpenALPR API")
return
# processing api data
# Processing API data
vehicles = 0
result = {}
@@ -1,19 +1,19 @@
"""
Component that will help set the OpenALPR local for ALPR processing.
For more details about this component, please refer to the documentation at
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/image_processing.openalpr_local/
"""
import asyncio
import logging
import io
import logging
import re
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.core import split_entity_id, callback
from homeassistant.const import STATE_UNKNOWN
import homeassistant.helpers.config_validation as cv
from homeassistant.components.image_processing import (
PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE,
CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE)
@@ -45,15 +45,13 @@ OPENALPR_REGIONS = [
'vn2'
]
CONF_REGION = 'region'
CONF_ALPR_BIN = 'alp_bin'
CONF_REGION = 'region'
DEFAULT_BINARY = 'alpr'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_REGION):
vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)),
vol.Required(CONF_REGION): vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)),
vol.Optional(CONF_ALPR_BIN, default=DEFAULT_BINARY): cv.string,
})
@@ -77,9 +75,9 @@ class ImageProcessingAlprEntity(ImageProcessingEntity):
"""Base entity class for ALPR image processing."""
def __init__(self):
"""Initialize base alpr entity."""
self.plates = {} # last scan data
self.vehicles = 0 # vehicles count
"""Initialize base ALPR entity."""
self.plates = {}
self.vehicles = 0
@property
def state(self):
@@ -128,7 +126,7 @@ class ImageProcessingAlprEntity(ImageProcessingEntity):
if confidence >= self.confidence}
new_plates = set(plates) - set(self.plates)
# send events
# Send events
for i_plate in new_plates:
self.hass.async_add_job(
self.hass.bus.async_fire, EVENT_FOUND_PLATE, {
@@ -138,7 +136,7 @@ class ImageProcessingAlprEntity(ImageProcessingEntity):
}
)
# update entity store
# Update entity store
self.plates = plates
self.vehicles = vehicles
@@ -192,7 +190,7 @@ class OpenAlprLocalEntity(ImageProcessingAlprEntity):
stderr=asyncio.subprocess.DEVNULL
)
# send image
# Send image
stdout, _ = yield from alpr.communicate(input=image)
stdout = io.StringIO(str(stdout, 'utf-8'))
@@ -204,12 +202,12 @@ class OpenAlprLocalEntity(ImageProcessingAlprEntity):
new_plates = RE_ALPR_PLATE.search(line)
new_result = RE_ALPR_RESULT.search(line)
# found new vehicle
# Found new vehicle
if new_plates:
vehicles += 1
continue
# found plate result
# Found plate result
if new_result:
try:
result.update(
@@ -1,7 +1,7 @@
"""
Component that performs OpenCV classification on images.
For more details about this component, please refer to the documentation at
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/image_processing.opencv/
"""
from datetime import timedelta
@@ -9,22 +9,15 @@ import logging
from homeassistant.core import split_entity_id
from homeassistant.components.image_processing import (
ImageProcessingEntity,
PLATFORM_SCHEMA,
)
ImageProcessingEntity, PLATFORM_SCHEMA)
from homeassistant.components.opencv import (
ATTR_MATCHES,
CLASSIFIER_GROUP_CONFIG,
CONF_CLASSIFIER,
CONF_ENTITY_ID,
CONF_NAME,
process_image,
)
DEPENDENCIES = ['opencv']
ATTR_MATCHES, CLASSIFIER_GROUP_CONFIG, CONF_CLASSIFIER, CONF_ENTITY_ID,
CONF_NAME, process_image)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['opencv']
DEFAULT_TIMEOUT = 10
SCAN_INTERVAL = timedelta(seconds=2)
@@ -33,18 +26,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(CLASSIFIER_GROUP_CONFIG)
def _create_processor_from_config(hass, camera_entity, config):
"""Create an OpenCV processor from configurtaion."""
"""Create an OpenCV processor from configuration."""
classifier_config = config[CONF_CLASSIFIER]
name = '{} {}'.format(
config[CONF_NAME],
split_entity_id(camera_entity)[1].replace('_', ' '))
config[CONF_NAME], split_entity_id(camera_entity)[1].replace('_', ' '))
processor = OpenCVImageProcessor(
hass,
camera_entity,
name,
classifier_config,
)
hass, camera_entity, name, classifier_config)
return processor
@@ -57,10 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
devices = []
for camera_entity in discovery_info[CONF_ENTITY_ID]:
devices.append(
_create_processor_from_config(
hass,
camera_entity,
discovery_info))
_create_processor_from_config(hass, camera_entity, discovery_info))
add_devices(devices)
@@ -115,6 +100,5 @@ class OpenCVImageProcessor(ImageProcessingEntity):
def process_image(self, image):
"""Process the image."""
self._last_image = image
self._matches = process_image(image,
self._classifier_configs,
False)
self._matches = process_image(
image, self._classifier_configs, False)
@@ -0,0 +1,114 @@
"""
Local optical character recognition processing of seven segements displays.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/image_processing.seven_segments/
"""
import asyncio
import logging
import io
import os
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.core import split_entity_id
from homeassistant.components.image_processing import (
PLATFORM_SCHEMA, ImageProcessingEntity, CONF_SOURCE, CONF_ENTITY_ID,
CONF_NAME)
_LOGGER = logging.getLogger(__name__)
CONF_DIGITS = 'digits'
CONF_HEIGHT = 'height'
CONF_SSOCR_BIN = 'ssocr_bin'
CONF_THRESHOLD = 'threshold'
CONF_WIDTH = 'width'
CONF_X_POS = 'x_position'
CONF_Y_POS = 'y_position'
DEFAULT_BINARY = 'ssocr'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DIGITS, default=-1): cv.positive_int,
vol.Optional(CONF_HEIGHT, default=0): cv.positive_int,
vol.Optional(CONF_SSOCR_BIN, default=DEFAULT_BINARY): cv.string,
vol.Optional(CONF_THRESHOLD, default=0): cv.positive_int,
vol.Optional(CONF_WIDTH, default=0): cv.positive_int,
vol.Optional(CONF_X_POS, default=0): cv.string,
vol.Optional(CONF_Y_POS, default=0): cv.positive_int,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Seven segments OCR platform."""
entities = []
for camera in config[CONF_SOURCE]:
entities.append(ImageProcessingSsocr(
hass, camera[CONF_ENTITY_ID], config, camera.get(CONF_NAME)
))
async_add_devices(entities)
class ImageProcessingSsocr(ImageProcessingEntity):
"""Representation of the seven segments OCR image processing entity."""
def __init__(self, hass, camera_entity, config, name):
"""Initialize seven segments processing."""
self.hass = hass
self._camera_entity = camera_entity
if name:
self._name = name
else:
self._name = "SevenSegement OCR {0}".format(
split_entity_id(camera_entity)[1])
self._state = None
self.filepath = os.path.join(self.hass.config.config_dir, 'ocr.png')
self._command = [
config[CONF_SSOCR_BIN], 'erosion', 'make_mono', 'crop',
str(config[CONF_X_POS]), str(config[CONF_Y_POS]),
str(config[CONF_WIDTH]), str(config[CONF_HEIGHT]), '-t',
str(config[CONF_THRESHOLD]), '-d', str(config[CONF_DIGITS]),
self.filepath
]
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return 'ocr'
@property
def camera_entity(self):
"""Return camera entity id from process pictures."""
return self._camera_entity
@property
def name(self):
"""Return the name of the image processor."""
return self._name
@property
def state(self):
"""Return the state of the entity."""
return self._state
def process_image(self, image):
"""Process the image."""
from PIL import Image
import subprocess
stream = io.BytesIO(image)
img = Image.open(stream)
img.save(self.filepath, 'png')
ocr = subprocess.Popen(
self._command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out = ocr.communicate()
if out[0] != b'':
self._state = out[0].strip().decode('utf-8')
else:
self._state = None
_LOGGER.warning(
"Unable to detect value: %s", out[1].strip().decode('utf-8'))
+142
View File
@@ -0,0 +1,142 @@
"""KIRA interface to receive UDP packets from an IR-IP bridge."""
# pylint: disable=import-error
import logging
import os
import yaml
import voluptuous as vol
from voluptuous.error import Error as VoluptuousError
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_DEVICE,
CONF_HOST,
CONF_NAME,
CONF_PORT,
CONF_SENSORS,
CONF_TYPE,
EVENT_HOMEASSISTANT_STOP,
STATE_UNKNOWN)
REQUIREMENTS = ["pykira==0.1.1"]
DOMAIN = 'kira'
_LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 65432
CONF_CODE = "code"
CONF_REPEAT = "repeat"
CONF_REMOTES = "remotes"
CONF_SENSOR = "sensor"
CONF_REMOTE = "remote"
CODES_YAML = '{}_codes.yaml'.format(DOMAIN)
CODE_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_CODE): cv.string,
vol.Optional(CONF_TYPE): cv.string,
vol.Optional(CONF_DEVICE): cv.string,
vol.Optional(CONF_REPEAT): cv.positive_int,
})
SENSOR_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME, default=DOMAIN):
vol.Exclusive(cv.string, "sensors"),
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
REMOTE_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME, default=DOMAIN):
vol.Exclusive(cv.string, "remotes"),
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_SENSORS): [SENSOR_SCHEMA],
vol.Optional(CONF_REMOTES): [REMOTE_SCHEMA]})
}, extra=vol.ALLOW_EXTRA)
def load_codes(path):
"""Load Kira codes from specified file."""
codes = []
if os.path.exists(path):
with open(path) as code_file:
data = yaml.load(code_file) or []
for code in data:
try:
codes.append(CODE_SCHEMA(code))
except VoluptuousError as exception:
# keep going
_LOGGER.warning('Kira Code Invalid Data: %s', exception)
else:
with open(path, 'w') as code_file:
code_file.write('')
return codes
def setup(hass, config):
"""Setup KIRA capability."""
import pykira
sensors = config.get(DOMAIN, {}).get(CONF_SENSORS, [])
remotes = config.get(DOMAIN, {}).get(CONF_REMOTES, [])
# If no sensors or remotes were specified, add a sensor
if not(sensors or remotes):
sensors.append({})
codes = load_codes(hass.config.path(CODES_YAML))
hass.data[DOMAIN] = {
CONF_SENSOR: {},
CONF_REMOTE: {},
}
def load_module(platform, idx, module_conf):
"""Set up Kira module and load platform."""
# note: module_name is not the HA device name. it's just a unique name
# to ensure the component and platform can share information
module_name = ("%s_%d" % (DOMAIN, idx)) if idx else DOMAIN
device_name = module_conf.get(CONF_NAME, DOMAIN)
port = module_conf.get(CONF_PORT, DEFAULT_PORT)
host = module_conf.get(CONF_HOST, DEFAULT_HOST)
if platform == CONF_SENSOR:
module = pykira.KiraReceiver(host, port)
module.start()
else:
module = pykira.KiraModule(host, port)
hass.data[DOMAIN][platform][module_name] = module
for code in codes:
code_tuple = (code.get(CONF_NAME),
code.get(CONF_DEVICE, STATE_UNKNOWN))
module.registerCode(code_tuple, code.get(CONF_CODE))
discovery.load_platform(hass, platform, DOMAIN,
{'name': module_name, 'device': device_name},
config)
for idx, module_conf in enumerate(sensors):
load_module(CONF_SENSOR, idx, module_conf)
for idx, module_conf in enumerate(remotes):
load_module(CONF_REMOTE, idx, module_conf)
def _stop_kira(_event):
for receiver in hass.data[DOMAIN][CONF_SENSOR].values():
receiver.stop()
_LOGGER.info("Terminated receivers")
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_kira)
return True
+81 -44
View File
@@ -50,13 +50,15 @@ ATTR_TRANSITION = "transition"
ATTR_RGB_COLOR = "rgb_color"
ATTR_XY_COLOR = "xy_color"
ATTR_COLOR_TEMP = "color_temp"
ATTR_KELVIN = "kelvin"
ATTR_MIN_MIREDS = "min_mireds"
ATTR_MAX_MIREDS = "max_mireds"
ATTR_COLOR_NAME = "color_name"
ATTR_WHITE_VALUE = "white_value"
# int with value 0 .. 255 representing brightness of the light.
# Brightness of the light, 0..255 or percentage
ATTR_BRIGHTNESS = "brightness"
ATTR_BRIGHTNESS_PCT = "brightness_pct"
# String representing a profile (built-in ones or external defined).
ATTR_PROFILE = "profile"
@@ -92,18 +94,21 @@ PROP_TO_ATTR = {
# Service call validation schemas
VALID_TRANSITION = vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553))
VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255))
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,
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)),
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,
@@ -142,20 +147,21 @@ def is_on(hass, entity_id=None):
def turn_on(hass, entity_id=None, transition=None, brightness=None,
rgb_color=None, xy_color=None, color_temp=None, white_value=None,
brightness_pct=None, rgb_color=None, xy_color=None,
color_temp=None, kelvin=None, white_value=None,
profile=None, flash=None, effect=None, color_name=None):
"""Turn all or specified light on."""
hass.add_job(
async_turn_on, hass, entity_id, transition, brightness,
rgb_color, xy_color, color_temp, white_value,
async_turn_on, hass, entity_id, transition, brightness, brightness_pct,
rgb_color, xy_color, color_temp, kelvin, white_value,
profile, flash, effect, color_name)
@callback
def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
rgb_color=None, xy_color=None, color_temp=None,
white_value=None, profile=None, flash=None, effect=None,
color_name=None):
brightness_pct=None, rgb_color=None, xy_color=None,
color_temp=None, kelvin=None, white_value=None,
profile=None, flash=None, effect=None, color_name=None):
"""Turn all or specified light on."""
data = {
key: value for key, value in [
@@ -163,9 +169,11 @@ def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
(ATTR_PROFILE, profile),
(ATTR_TRANSITION, transition),
(ATTR_BRIGHTNESS, brightness),
(ATTR_BRIGHTNESS_PCT, brightness_pct),
(ATTR_RGB_COLOR, rgb_color),
(ATTR_XY_COLOR, xy_color),
(ATTR_COLOR_TEMP, color_temp),
(ATTR_KELVIN, kelvin),
(ATTR_WHITE_VALUE, white_value),
(ATTR_FLASH, flash),
(ATTR_EFFECT, effect),
@@ -207,6 +215,27 @@ def toggle(hass, entity_id=None, transition=None):
hass.services.call(DOMAIN, SERVICE_TOGGLE, data)
def preprocess_turn_on_alternatives(params):
"""Processing extra data for turn light on request."""
profile = Profiles.get(params.pop(ATTR_PROFILE, None))
if profile is not None:
params.setdefault(ATTR_XY_COLOR, profile[:2])
params.setdefault(ATTR_BRIGHTNESS, profile[2])
color_name = params.pop(ATTR_COLOR_NAME, None)
if color_name is not None:
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
kelvin = params.pop(ATTR_KELVIN, None)
if kelvin is not None:
mired = color_util.color_temperature_kelvin_to_mired(kelvin)
params[ATTR_COLOR_TEMP] = int(mired)
brightness_pct = params.pop(ATTR_BRIGHTNESS_PCT, None)
if brightness_pct is not None:
params[ATTR_BRIGHTNESS] = int(255 * brightness_pct/100)
@asyncio.coroutine
def async_setup(hass, config):
"""Expose light control via statemachine and services."""
@@ -215,10 +244,8 @@ def async_setup(hass, config):
yield from component.async_setup(config)
# load profiles from files
profiles = yield from hass.loop.run_in_executor(
None, _load_profile_data, hass)
if profiles is None:
profiles_valid = yield from Profiles.load_profiles(hass)
if not profiles_valid:
return False
@asyncio.coroutine
@@ -231,17 +258,7 @@ def async_setup(hass, config):
target_lights = component.async_extract_from_service(service)
params.pop(ATTR_ENTITY_ID, None)
# Processing extra data for turn light on request.
profile = profiles.get(params.pop(ATTR_PROFILE, None))
if profile:
params.setdefault(ATTR_XY_COLOR, profile[:2])
params.setdefault(ATTR_BRIGHTNESS, profile[2])
color_name = params.pop(ATTR_COLOR_NAME, None)
if color_name is not None:
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
preprocess_turn_on_alternatives(params)
for light in target_lights:
if service.service == SERVICE_TURN_ON:
@@ -287,31 +304,51 @@ def async_setup(hass, config):
return True
def _load_profile_data(hass):
"""Load built-in profiles and custom profiles."""
profile_paths = [os.path.join(os.path.dirname(__file__),
LIGHT_PROFILES_FILE),
hass.config.path(LIGHT_PROFILES_FILE)]
profiles = {}
class Profiles:
"""Representation of available color profiles."""
for profile_path in profile_paths:
if not os.path.isfile(profile_path):
continue
with open(profile_path) as inp:
reader = csv.reader(inp)
_all = None
# Skip the header
next(reader, None)
@classmethod
@asyncio.coroutine
def load_profiles(cls, hass):
"""Load and cache profiles."""
def load_profile_data(hass):
"""Load built-in profiles and custom profiles."""
profile_paths = [os.path.join(os.path.dirname(__file__),
LIGHT_PROFILES_FILE),
hass.config.path(LIGHT_PROFILES_FILE)]
profiles = {}
try:
for rec in reader:
profile, color_x, color_y, brightness = PROFILE_SCHEMA(rec)
profiles[profile] = (color_x, color_y, brightness)
except vol.MultipleInvalid as ex:
_LOGGER.error("Error parsing light profile from %s: %s",
profile_path, ex)
return None
return profiles
for profile_path in profile_paths:
if not os.path.isfile(profile_path):
continue
with open(profile_path) as inp:
reader = csv.reader(inp)
# Skip the header
next(reader, None)
try:
for rec in reader:
profile, color_x, color_y, brightness = \
PROFILE_SCHEMA(rec)
profiles[profile] = (color_x, color_y, brightness)
except vol.MultipleInvalid as ex:
_LOGGER.error(
"Error parsing light profile from %s: %s",
profile_path, ex)
return None
return profiles
cls._all = yield from hass.loop.run_in_executor(
None, load_profile_data, hass)
return cls._all is not None
@classmethod
def get(cls, name):
"""Return a named profile."""
return cls._all.get(name)
class Light(ToggleEntity):
@@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Add device specified by serial number."""
"""Set up Blinkstick device specified by serial number."""
from blinkstick import blinkstick
name = config.get(CONF_NAME)
+1
View File
@@ -29,6 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Blinkt Light platform."""
# pylint: disable=import-error
import blinkt
# ensure that the lights are off when exiting
+2 -3
View File
@@ -20,14 +20,13 @@ _LOGGER = logging.getLogger(__name__)
CONF_SENDER_ID = 'sender_id'
DEFAULT_NAME = 'EnOcean Light'
DEPENDENCIES = ['enocean']
SUPPORT_ENOCEAN = SUPPORT_BRIGHTNESS
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_ID, default=[]): vol.All(cv.ensure_list,
[vol.Coerce(int)]),
vol.Optional(CONF_ID, default=[]):
vol.All(cv.ensure_list, [vol.Coerce(int)]),
vol.Required(CONF_SENDER_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
+22 -22
View File
@@ -35,26 +35,26 @@ SUPPORT_FLUX_LED_RGBW = (SUPPORT_WHITE_VALUE | SUPPORT_EFFECT |
MODE_RGB = 'rgb'
MODE_RGBW = 'rgbw'
# List of Supported Effects which aren't already declared in LIGHT
EFFECT_RED_FADE = "red_fade"
EFFECT_GREEN_FADE = "green_fade"
EFFECT_BLUE_FADE = "blue_fade"
EFFECT_YELLOW_FADE = "yellow_fade"
EFFECT_CYAN_FADE = "cyan_fade"
EFFECT_PURPLE_FADE = "purple_fade"
EFFECT_WHITE_FADE = "white_fade"
EFFECT_RED_GREEN_CROSS_FADE = "rg_cross_fade"
EFFECT_RED_BLUE_CROSS_FADE = "rb_cross_fade"
EFFECT_GREEN_BLUE_CROSS_FADE = "gb_cross_fade"
EFFECT_COLORSTROBE = "colorstrobe"
EFFECT_RED_STROBE = "red_strobe"
EFFECT_GREEN_STROBE = "green_strobe"
EFFECT_BLUE_STOBE = "blue_strobe"
EFFECT_YELLOW_STROBE = "yellow_strobe"
EFFECT_CYAN_STROBE = "cyan_strobe"
EFFECT_PURPLE_STROBE = "purple_strobe"
EFFECT_WHITE_STROBE = "white_strobe"
EFFECT_COLORJUMP = "colorjump"
# List of supported effects which aren't already declared in LIGHT
EFFECT_RED_FADE = 'red_fade'
EFFECT_GREEN_FADE = 'green_fade'
EFFECT_BLUE_FADE = 'blue_fade'
EFFECT_YELLOW_FADE = 'yellow_fade'
EFFECT_CYAN_FADE = 'cyan_fade'
EFFECT_PURPLE_FADE = 'purple_fade'
EFFECT_WHITE_FADE = 'white_fade'
EFFECT_RED_GREEN_CROSS_FADE = 'rg_cross_fade'
EFFECT_RED_BLUE_CROSS_FADE = 'rb_cross_fade'
EFFECT_GREEN_BLUE_CROSS_FADE = 'gb_cross_fade'
EFFECT_COLORSTROBE = 'colorstrobe'
EFFECT_RED_STROBE = 'red_strobe'
EFFECT_GREEN_STROBE = 'green_strobe'
EFFECT_BLUE_STOBE = 'blue_strobe'
EFFECT_YELLOW_STROBE = 'yellow_strobe'
EFFECT_CYAN_STROBE = 'cyan_strobe'
EFFECT_PURPLE_STROBE = 'purple_strobe'
EFFECT_WHITE_STROBE = 'white_strobe'
EFFECT_COLORJUMP = 'colorjump'
FLUX_EFFECT_LIST = [
EFFECT_COLORLOOP,
@@ -121,7 +121,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
ipaddr = device['ipaddr']
if ipaddr in light_ips:
continue
device['name'] = device['id'] + " " + ipaddr
device['name'] = '{} {}'.format(device['id'], ipaddr)
device[ATTR_MODE] = 'rgbw'
device[CONF_PROTOCOL] = None
light = FluxLight(device)
@@ -167,7 +167,7 @@ class FluxLight(Light):
@property
def unique_id(self):
"""Return the ID of this light."""
return "{}.{}".format(self.__class__, self._ipaddr)
return '{}.{}'.format(self.__class__, self._ipaddr)
@property
def name(self):
@@ -84,7 +84,7 @@ def setup_light(device_id, name, insteonhub, hass, add_devices_callback):
request_id = _CONFIGURING.pop(device_id)
configurator = get_component('configurator')
configurator.request_done(request_id)
_LOGGER.info("Device configuration done!")
_LOGGER.debug("Device configuration done")
conf_lights = config_from_file(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF))
if device_id not in conf_lights:
@@ -107,7 +107,7 @@ def config_from_file(filename, config=None):
with open(filename, 'w') as fdesc:
fdesc.write(json.dumps(config))
except IOError as error:
_LOGGER.error('Saving config file failed: %s', error)
_LOGGER.error("Saving config file failed: %s", error)
return False
return True
else:
@@ -1,5 +1,5 @@
"""
Support for INSTEON lights via PowerLinc Modem.
Support for Insteon lights via PowerLinc Modem.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/insteon_plm/
@@ -12,16 +12,16 @@ from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
from homeassistant.loader import get_component
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['insteon_plm']
MAX_BRIGHTNESS = 255
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the INSTEON PLM device class for the hass platform."""
"""Set up the Insteon PLM device."""
plm = hass.data['insteon_plm']
device_list = []
@@ -30,7 +30,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
address = device.get('address_hex')
dimmable = bool('dimmable' in device.get('capabilities'))
_LOGGER.info('Registered %s with light platform.', name)
_LOGGER.info("Registered %s with light platform", name)
device_list.append(
InsteonPLMDimmerDevice(hass, plm, address, name, dimmable)
@@ -72,14 +72,14 @@ class InsteonPLMDimmerDevice(Light):
def brightness(self):
"""Return the brightness of this light between 0..255."""
onlevel = self._plm.get_device_attr(self._address, 'onlevel')
_LOGGER.debug('on level for %s is %s', self._address, onlevel)
_LOGGER.debug("on level for %s is %s", self._address, onlevel)
return int(onlevel)
@property
def is_on(self):
"""Return the boolean response if the node is on."""
onlevel = self._plm.get_device_attr(self._address, 'onlevel')
_LOGGER.debug('on level for %s is %s', self._address, onlevel)
_LOGGER.debug("on level for %s is %s", self._address, onlevel)
return bool(onlevel)
@property
@@ -101,7 +101,7 @@ class InsteonPLMDimmerDevice(Light):
@callback
def async_light_update(self, message):
"""Receive notification from transport that new data exists."""
_LOGGER.info('Received update calback from PLM for %s', self._address)
_LOGGER.info("Received update calback from PLM for %s", self._address)
self._hass.async_add_job(self.async_update_ha_state())
@asyncio.coroutine
+4 -5
View File
@@ -24,13 +24,12 @@ def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 light platform."""
if isy.ISY is None or not isy.ISY.connected:
_LOGGER.error('A connection has not been made to the ISY controller.')
_LOGGER.error("A connection has not been made to the ISY controller")
return False
devices = []
for node in isy.filter_nodes(isy.NODES, units=UOM,
states=STATES):
for node in isy.filter_nodes(isy.NODES, units=UOM, states=STATES):
if node.dimmable or '51' in node.uom:
devices.append(ISYLightDevice(node))
@@ -57,12 +56,12 @@ class ISYLightDevice(isy.ISYDevice, Light):
def turn_off(self, **kwargs) -> None:
"""Send the turn off command to the ISY994 light device."""
if not self._node.off():
_LOGGER.debug('Unable to turn on light.')
_LOGGER.debug("Unable to turn on light")
def turn_on(self, brightness=None, **kwargs) -> None:
"""Send the turn on command to the ISY994 light device."""
if not self._node.on(val=brightness):
_LOGGER.debug('Unable to turn on light.')
_LOGGER.debug("Unable to turn on light")
@property
def supported_features(self):
+84 -25
View File
@@ -9,6 +9,7 @@ import logging
import asyncio
import sys
import math
from os import path
from functools import partial
from datetime import timedelta
import async_timeout
@@ -16,15 +17,19 @@ import async_timeout
import voluptuous as vol
from homeassistant.components.light import (
Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR,
Light, DOMAIN, PLATFORM_SCHEMA, LIGHT_TURN_ON_SCHEMA,
ATTR_BRIGHTNESS, ATTR_RGB_COLOR,
ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_EFFECT,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT)
SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT,
preprocess_turn_on_alternatives)
from homeassistant.config import load_yaml_config_file
from homeassistant.util.color import (
color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired)
from homeassistant import util
from homeassistant.core import callback
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.service import extract_entity_ids
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
@@ -41,7 +46,10 @@ BULB_LATENCY = 500
CONF_SERVER = 'server'
SERVICE_LIFX_SET_STATE = 'lifx_set_state'
ATTR_HSBK = 'hsbk'
ATTR_POWER = 'power'
BYTE_MAX = 255
SHORT_MAX = 65535
@@ -53,6 +61,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
})
LIFX_SET_STATE_SCHEMA = LIGHT_TURN_ON_SCHEMA.extend({
ATTR_POWER: cv.boolean,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
@@ -87,6 +99,41 @@ class LIFXManager(object):
self.hass = hass
self.async_add_devices = async_add_devices
@asyncio.coroutine
def async_service_handle(service):
"""Apply a service."""
tasks = []
for light in self.service_to_entities(service):
if service.service == SERVICE_LIFX_SET_STATE:
task = light.async_set_state(**service.data)
tasks.append(hass.async_add_job(task))
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
descriptions = self.get_descriptions()
hass.services.async_register(
DOMAIN, SERVICE_LIFX_SET_STATE, async_service_handle,
descriptions.get(SERVICE_LIFX_SET_STATE),
schema=LIFX_SET_STATE_SCHEMA)
@staticmethod
def get_descriptions():
"""Load and return descriptions for our own service calls."""
return load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
def service_to_entities(self, service):
"""Return the known devices that a service call mentions."""
entity_ids = extract_entity_ids(self.hass, service)
if entity_ids:
entities = [entity for entity in self.entities.values()
if entity.entity_id in entity_ids]
else:
entities = list(self.entities.values())
return entities
@callback
def register(self, device):
"""Handle for newly detected bulb."""
@@ -298,6 +345,18 @@ class LIFXLight(Light):
@asyncio.coroutine
def async_turn_on(self, **kwargs):
"""Turn the device on."""
kwargs[ATTR_POWER] = True
yield from self.async_set_state(**kwargs)
@asyncio.coroutine
def async_turn_off(self, **kwargs):
"""Turn the device off."""
kwargs[ATTR_POWER] = False
yield from self.async_set_state(**kwargs)
@asyncio.coroutine
def async_set_state(self, **kwargs):
"""Set a color on the light and turn it on/off."""
yield from self.stop_effect()
if ATTR_EFFECT in kwargs:
@@ -309,39 +368,41 @@ class LIFXLight(Light):
else:
fade = 0
# These are both False if ATTR_POWER is not set
power_on = kwargs.get(ATTR_POWER, False)
power_off = not kwargs.get(ATTR_POWER, True)
hsbk, changed_color = self.find_hsbk(**kwargs)
_LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d",
self.who, self._power, fade, *hsbk)
if self._power == 0:
if power_off:
self.device.set_power(False, None, 0)
if changed_color:
self.device.set_color(hsbk, None, 0)
self.device.set_power(True, None, fade)
if power_on:
self.device.set_power(True, None, fade)
else:
self.device.set_power(True, None, 0) # racing for power status
if power_on:
self.device.set_power(True, None, 0)
if changed_color:
self.device.set_color(hsbk, None, fade)
if power_off:
self.device.set_power(False, None, fade)
self.update_later(0)
if fade < BULB_LATENCY:
self.set_power(1)
self.set_color(*hsbk)
@asyncio.coroutine
def async_turn_off(self, **kwargs):
"""Turn the device off."""
yield from self.stop_effect()
if ATTR_TRANSITION in kwargs:
fade = int(kwargs[ATTR_TRANSITION] * 1000)
if power_on:
self.update_later(0)
else:
fade = 0
self.update_later(fade)
self.device.set_power(False, None, fade)
self.update_later(fade)
if fade < BULB_LATENCY:
self.set_power(0)
if fade <= BULB_LATENCY:
if power_on:
self.set_power(1)
if power_off:
self.set_power(0)
if changed_color:
self.set_color(*hsbk)
@asyncio.coroutine
def async_update(self):
@@ -374,9 +435,7 @@ class LIFXLight(Light):
if hsbk is not None:
return [hsbk, True]
color_name = kwargs.pop(ATTR_COLOR_NAME, None)
if color_name is not None:
kwargs[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
preprocess_turn_on_alternatives(kwargs)
if ATTR_RGB_COLOR in kwargs:
hue, saturation, brightness = \
+13 -20
View File
@@ -2,16 +2,14 @@
import logging
import asyncio
import random
from os import path
import voluptuous as vol
from homeassistant.components.light import (
DOMAIN, ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR, ATTR_EFFECT,
ATTR_TRANSITION)
from homeassistant.config import load_yaml_config_file
DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME,
ATTR_RGB_COLOR, ATTR_EFFECT, ATTR_TRANSITION,
VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT)
from homeassistant.const import (ATTR_ENTITY_ID)
from homeassistant.helpers.service import extract_entity_ids
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -39,7 +37,8 @@ LIFX_EFFECT_SCHEMA = vol.Schema({
})
LIFX_EFFECT_BREATHE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
ATTR_BRIGHTNESS: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
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)),
@@ -52,7 +51,8 @@ LIFX_EFFECT_BREATHE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_BREATHE_SCHEMA
LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
ATTR_BRIGHTNESS: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
vol.Optional(ATTR_PERIOD, default=60):
vol.All(vol.Coerce(float), vol.Clamp(min=0.05)),
vol.Optional(ATTR_CHANGE, default=20):
@@ -73,19 +73,12 @@ def setup(hass, lifx_manager):
@asyncio.coroutine
def async_service_handle(service):
"""Apply a service."""
entity_ids = extract_entity_ids(hass, service)
if entity_ids:
devices = [entity for entity in lifx_manager.entities.values()
if entity.entity_id in entity_ids]
else:
devices = list(lifx_manager.entities.values())
if devices:
yield from start_effect(hass, devices,
entities = lifx_manager.service_to_entities(service)
if entities:
yield from start_effect(hass, entities,
service.service, **service.data)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
descriptions = lifx_manager.get_descriptions()
hass.services.async_register(
DOMAIN, SERVICE_EFFECT_BREATHE, async_service_handle,
@@ -292,7 +285,7 @@ class LIFXEffectColorloop(LIFXEffect):
direction = 1 if random.randint(0, 1) else -1
# Random start
hue = random.randint(0, 359)
hue = random.uniform(0, 360) % 360
while self.lights:
hue = (hue + direction*change) % 360
@@ -312,7 +305,7 @@ class LIFXEffectColorloop(LIFXEffect):
brightness = light.effect_data.color[2]
hsbk = [
int(65535/359*lhue),
int(65535/360*lhue),
int(random.uniform(0.8, 1.0)*65535),
brightness,
NEUTRAL_WHITE,
@@ -1,3 +1,23 @@
lifx_set_state:
description: Set a color/brightness and possibliy turn the light on/off
fields:
entity_id:
description: Name(s) of entities to set a state on
example: 'light.garage'
'...':
description: All turn_on parameters can be used to specify a color
transition:
description: Duration in seconds it takes to get to the final state
example: 10
power:
description: Turn the light on (True) or off (False). Leave out to keep the power as it is.
example: True
lifx_effect_breathe:
description: Run a breathe effect by fading to a color and back.
@@ -4,7 +4,6 @@ Support for LimitlessLED bulbs.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.limitlessled/
"""
import logging
import voluptuous as vol
@@ -17,7 +16,7 @@ from homeassistant.components.light import (
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['limitlessled==1.0.5']
REQUIREMENTS = ['limitlessled==1.0.8']
_LOGGER = logging.getLogger(__name__)
@@ -195,7 +195,7 @@ class Luminary(Light):
self._brightness = kwargs[ATTR_BRIGHTNESS]
_LOGGER.debug("turn_on requested brightness for light: %s is: %s ",
self._name, self._brightness)
self._brightness = self._luminary.set_luminance(
self._luminary.set_luminance(
int(self._brightness / 2.55),
transition)
+9 -1
View File
@@ -26,7 +26,11 @@ turn_on:
color_temp:
description: Color temperature for the light in mireds
example: '250'
example: 250
kelvin:
description: Color temperature for the light in Kelvin
example: 4000
white_value:
description: Number between 0..255 indicating level of white
@@ -36,6 +40,10 @@ turn_on:
description: Number between 0..255 indicating brightness
example: 120
brightness_pct:
description: Number between 0..100 indicating percentage of full brightness
example: 47
profile:
description: Name of a light profile to use
example: relax
+11 -4
View File
@@ -1,4 +1,9 @@
"""Support for the IKEA Tradfri platform."""
"""
Support for the IKEA Tradfri platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.tradfri/
"""
import logging
from homeassistant.components.light import (
@@ -6,7 +11,7 @@ from homeassistant.components.light import (
SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light)
from homeassistant.components.light import \
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA
from homeassistant.components.tradfri import KEY_GATEWAY
from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS
from homeassistant.util import color as color_util
_LOGGER = logging.getLogger(__name__)
@@ -30,8 +35,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
lights = [dev for dev in devices if dev.has_light_control]
add_devices(Tradfri(light) for light in lights)
groups = gateway.get_groups()
add_devices(TradfriGroup(group) for group in groups)
allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id]
if allow_tradfri_groups:
groups = gateway.get_groups()
add_devices(TradfriGroup(group) for group in groups)
class TradfriGroup(Light):
+6
View File
@@ -4,6 +4,7 @@ Support for Wink lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.wink/
"""
import asyncio
import colorsys
from homeassistant.components.light import (
@@ -38,6 +39,11 @@ class WinkLight(WinkDevice, Light):
"""Initialize the Wink device."""
super().__init__(wink, hass)
@asyncio.coroutine
def async_added_to_hass(self):
"""Callback when entity is added to hass."""
self.hass.data[DOMAIN]['entities']['light'].append(self)
@property
def is_on(self):
"""Return true if light is on."""
@@ -55,3 +55,59 @@ unlock:
code:
description: An optional code to unlock the lock with
example: 1234
wink_set_lock_vacation_mode:
description: Set vacation mode for all or specified locks. Disables all user codes.
fields:
entity_id:
description: Name of lock to unlock
example: 'lock.front_door'
enabled:
description: enable or disable. true or false.
example: true
wink_set_lock_alarm_mode:
description: Set alarm mode for all or specified locks.
fields:
entity_id:
description: Name of lock to unlock
example: 'lock.front_door'
mode:
description: One of tamper, activity, or forced_entry
example: tamper
wink_set_lock_alarm_sensitivity:
description: Set alarm sensitivity for all or specified locks.
fields:
entity_id:
description: Name of lock to unlock
example: 'lock.front_door'
sensitivity:
description: One of low, medium_low, medium, medium_high, high
example: medium
wink_set_lock_alarm_state:
description: Set alarm state.
fields:
entity_id:
description: Name of lock to unlock
example: 'lock.front_door'
enabled:
description: enable or disable. true or false.
example: true
wink_set_lock_beeper_state:
description: Set beeper state.
fields:
entity_id:
description: Name of lock to unlock
example: 'lock.front_door'
enabled:
description: enable or disable. true or false.
example: true
+158
View File
@@ -4,11 +4,55 @@ Support for Wink locks.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/lock.wink/
"""
import asyncio
import logging
from os import path
import voluptuous as vol
from homeassistant.components.lock import LockDevice
from homeassistant.components.wink import WinkDevice, DOMAIN
import homeassistant.helpers.config_validation as cv
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN
from homeassistant.config import load_yaml_config_file
DEPENDENCIES = ['wink']
_LOGGER = logging.getLogger(__name__)
SERVICE_SET_VACATION_MODE = 'wink_set_lock_vacation_mode'
SERVICE_SET_ALARM_MODE = 'wink_set_lock_alarm_mode'
SERVICE_SET_ALARM_SENSITIVITY = 'wink_set_lock_alarm_sensitivity'
SERVICE_SET_ALARM_STATE = 'wink_set_lock_alarm_state'
SERVICE_SET_BEEPER_STATE = 'wink_set_lock_beeper_state'
ATTR_ENABLED = 'enabled'
ATTR_SENSITIVITY = 'sensitivity'
ATTR_MODE = 'mode'
ALARM_SENSITIVITY_MAP = {"low": 0.2, "medium_low": 0.4,
"medium": 0.6, "medium_high": 0.8,
"high": 1.0}
ALARM_MODES_MAP = {"tamper": "tamper",
"activity": "alert",
"forced_entry": "forced_entry"}
SET_ENABLED_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_ENABLED): cv.string,
})
SET_SENSITIVITY_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_SENSITIVITY): vol.In(ALARM_SENSITIVITY_MAP)
})
SET_ALARM_MODES_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_MODE): vol.In(ALARM_MODES_MAP)
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Wink platform."""
@@ -19,6 +63,58 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if _id not in hass.data[DOMAIN]['unique_ids']:
add_devices([WinkLockDevice(lock, hass)])
def service_handle(service):
"""Handler for services."""
entity_ids = service.data.get('entity_id')
all_locks = hass.data[DOMAIN]['entities']['lock']
locks_to_set = []
if entity_ids is None:
locks_to_set = all_locks
else:
for lock in all_locks:
if lock.entity_id in entity_ids:
locks_to_set.append(lock)
for lock in locks_to_set:
if service.service == SERVICE_SET_VACATION_MODE:
lock.set_vacation_mode(service.data.get(ATTR_ENABLED))
elif service.service == SERVICE_SET_ALARM_STATE:
lock.set_alarm_state(service.data.get(ATTR_ENABLED))
elif service.service == SERVICE_SET_BEEPER_STATE:
lock.set_beeper_state(service.data.get(ATTR_ENABLED))
elif service.service == SERVICE_SET_ALARM_MODE:
lock.set_alarm_mode(service.data.get(ATTR_MODE))
elif service.service == SERVICE_SET_ALARM_SENSITIVITY:
lock.set_alarm_sensitivity(service.data.get(ATTR_SENSITIVITY))
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_SET_VACATION_MODE,
service_handle,
descriptions.get(SERVICE_SET_VACATION_MODE),
schema=SET_ENABLED_SCHEMA)
hass.services.register(DOMAIN, SERVICE_SET_ALARM_STATE,
service_handle,
descriptions.get(SERVICE_SET_ALARM_STATE),
schema=SET_ENABLED_SCHEMA)
hass.services.register(DOMAIN, SERVICE_SET_BEEPER_STATE,
service_handle,
descriptions.get(SERVICE_SET_BEEPER_STATE),
schema=SET_ENABLED_SCHEMA)
hass.services.register(DOMAIN, SERVICE_SET_ALARM_MODE,
service_handle,
descriptions.get(SERVICE_SET_ALARM_MODE),
schema=SET_ALARM_MODES_SCHEMA)
hass.services.register(DOMAIN, SERVICE_SET_ALARM_SENSITIVITY,
service_handle,
descriptions.get(SERVICE_SET_ALARM_SENSITIVITY),
schema=SET_SENSITIVITY_SCHEMA)
class WinkLockDevice(WinkDevice, LockDevice):
"""Representation of a Wink lock."""
@@ -27,6 +123,11 @@ class WinkLockDevice(WinkDevice, LockDevice):
"""Initialize the lock."""
super().__init__(wink, hass)
@asyncio.coroutine
def async_added_to_hass(self):
"""Callback when entity is added to hass."""
self.hass.data[DOMAIN]['entities']['lock'].append(self)
@property
def is_locked(self):
"""Return true if device is locked."""
@@ -39,3 +140,60 @@ class WinkLockDevice(WinkDevice, LockDevice):
def unlock(self, **kwargs):
"""Unlock the device."""
self.wink.set_state(False)
def set_alarm_state(self, enabled):
"""Set lock's alarm state."""
self.wink.set_alarm_state(enabled)
def set_vacation_mode(self, enabled):
"""Set lock's vacation mode."""
self.wink.set_vacation_mode(enabled)
def set_beeper_state(self, enabled):
"""Set lock's beeper mode."""
self.wink.set_beeper_mode(enabled)
def set_alarm_sensitivity(self, sensitivity):
"""
Set lock's alarm sensitivity.
Valid sensitivities:
0.2, 0.4, 0.6, 0.8, 1.0
"""
self.wink.set_alarm_sensitivity(sensitivity)
def set_alarm_mode(self, mode):
"""
Set lock's alarm mode.
Valid modes:
alert - Beep when lock is locked or unlocked
tamper - 15 sec alarm when lock is disturbed when locked
forced_entry - 3 min alarm when significant force applied
to door when locked.
"""
self.wink.set_alarm_mode(mode)
@property
def device_state_attributes(self):
"""Return the state attributes."""
super_attrs = super().device_state_attributes
sensitivity = dict_value_to_key(ALARM_SENSITIVITY_MAP,
self.wink.alarm_sensitivity())
super_attrs['alarm sensitivity'] = sensitivity
super_attrs['vacation mode'] = self.wink.vacation_mode_enabled()
super_attrs['beeper mode'] = self.wink.beeper_enabled()
super_attrs['auto lock'] = self.wink.auto_lock_enabled()
alarm_mode = dict_value_to_key(ALARM_MODES_MAP,
self.wink.alarm_mode())
super_attrs['alarm mode'] = alarm_mode
super_attrs['alarm enabled'] = self.wink.alarm_enabled()
return super_attrs
def dict_value_to_key(dict_map, comp_value):
"""Return the key that has the provided value."""
for key, value in dict_map.items():
if value == comp_value:
return key
return STATE_UNKNOWN
+3 -2
View File
@@ -141,9 +141,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class_id=zwave.const.COMMAND_CLASS_USER_CODE).values():
if value.index != code_slot:
continue
if len(str(usercode)) > 4:
if len(str(usercode)) < 4:
_LOGGER.error("Invalid code provided: (%s) "
"usercode must %s or less digits",
"usercode must be atleast 4 and at most"
" %s digits",
usercode, len(value.data))
break
value.data = str(usercode)
+8 -6
View File
@@ -19,7 +19,8 @@ from homeassistant.components.frontend import register_built_in_panel
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
STATE_NOT_HOME, STATE_OFF, STATE_ON, ATTR_HIDDEN, HTTP_BAD_REQUEST)
STATE_NOT_HOME, STATE_OFF, STATE_ON, ATTR_HIDDEN, HTTP_BAD_REQUEST,
EVENT_LOGBOOK_ENTRY)
from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN
DOMAIN = 'logbook'
@@ -47,10 +48,10 @@ CONFIG_SCHEMA = vol.Schema({
}),
}, extra=vol.ALLOW_EXTRA)
EVENT_LOGBOOK_ENTRY = 'logbook_entry'
GROUP_BY_MINUTES = 15
CONTINUOUS_DOMAINS = ['proximity', 'sensor']
ATTR_NAME = 'name'
ATTR_MESSAGE = 'message'
ATTR_DOMAIN = 'domain'
@@ -191,7 +192,8 @@ def humanify(events):
if entity_id is None:
continue
if entity_id.startswith('sensor.'):
if entity_id.startswith(tuple('{}.'.format(
domain) for domain in CONTINUOUS_DOMAINS)):
last_sensor_event[entity_id] = event
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
@@ -223,12 +225,12 @@ def humanify(events):
domain = to_state.domain
# Skip all but the last sensor state
if domain == 'sensor' and \
if domain in CONTINUOUS_DOMAINS and \
event != last_sensor_event[to_state.entity_id]:
continue
# Don't show continuous sensor value changes in the logbook
if domain == 'sensor' and \
if domain in CONTINUOUS_DOMAINS and \
to_state.attributes.get('unit_of_measurement'):
continue
+2 -4
View File
@@ -14,9 +14,7 @@ from homeassistant.const import CONF_HOST
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['https://github.com/gurumitts/'
'pylutron-caseta/archive/v0.2.6.zip#'
'pylutron-caseta==v0.2.6']
REQUIREMENTS = ['pylutron-caseta==0.2.6']
_LOGGER = logging.getLogger(__name__)
@@ -46,7 +44,7 @@ def setup(hass, base_config):
_LOGGER.info("Connected to Lutron smartbridge at %s", config[CONF_HOST])
for component in ('light', 'switch'):
for component in ('light', 'switch', 'cover'):
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True
@@ -19,7 +19,7 @@ from homeassistant.const import (
CONF_NAME, STATE_ON)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['denonavr==0.4.0']
REQUIREMENTS = ['denonavr==0.4.1']
_LOGGER = logging.getLogger(__name__)
+109 -38
View File
@@ -9,20 +9,22 @@ from functools import wraps
import logging
import urllib
import re
import os
import aiohttp
import voluptuous as vol
from homeassistant.config import load_yaml_config_file
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, MediaPlayerDevice,
PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO,
MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN)
SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, SUPPORT_SHUFFLE_SET,
MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
MEDIA_TYPE_VIDEO, MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN)
from homeassistant.const import (
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME,
CONF_PORT, CONF_SSL, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD,
EVENT_HOMEASSISTANT_STOP)
CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
@@ -32,6 +34,8 @@ REQUIREMENTS = ['jsonrpc-async==0.6', 'jsonrpc-websocket==0.5']
_LOGGER = logging.getLogger(__name__)
EVENT_KODI_CALL_METHOD_RESULT = 'kodi_call_method_result'
CONF_TCP_PORT = 'tcp_port'
CONF_TURN_OFF_ACTION = 'turn_off_action'
CONF_ENABLE_WEBSOCKET = 'enable_websocket'
@@ -61,8 +65,9 @@ MEDIA_TYPES = {
}
SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_VOLUME_STEP
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_SHUFFLE_SET | \
SUPPORT_PLAY | SUPPORT_VOLUME_STEP
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
@@ -71,6 +76,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port,
vol.Optional(CONF_PROXY_SSL, default=DEFAULT_PROXY_SSL): cv.boolean,
vol.Optional(CONF_TURN_OFF_ACTION, default=None): vol.In(TURN_OFF_ACTION),
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Inclusive(CONF_USERNAME, 'auth'): cv.string,
vol.Inclusive(CONF_PASSWORD, 'auth'): cv.string,
vol.Optional(CONF_ENABLE_WEBSOCKET, default=DEFAULT_ENABLE_WEBSOCKET):
@@ -78,16 +84,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
SERVICE_ADD_MEDIA = 'kodi_add_to_playlist'
SERVICE_SET_SHUFFLE = 'kodi_set_shuffle'
SERVICE_CALL_METHOD = 'kodi_call_method'
DATA_KODI = 'kodi'
ATTR_MEDIA_TYPE = 'media_type'
ATTR_MEDIA_NAME = 'media_name'
ATTR_MEDIA_ARTIST_NAME = 'artist_name'
ATTR_MEDIA_ID = 'media_id'
MEDIA_PLAYER_SET_SHUFFLE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required('shuffle_on'): cv.boolean,
})
ATTR_METHOD = 'method'
MEDIA_PLAYER_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_MEDIA_TYPE): cv.string,
@@ -95,20 +100,25 @@ MEDIA_PLAYER_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Optional(ATTR_MEDIA_NAME): cv.string,
vol.Optional(ATTR_MEDIA_ARTIST_NAME): cv.string,
})
MEDIA_PLAYER_CALL_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_METHOD): cv.string,
}, extra=vol.ALLOW_EXTRA)
SERVICE_TO_METHOD = {
SERVICE_ADD_MEDIA: {
'method': 'async_add_media_to_playlist',
'schema': MEDIA_PLAYER_ADD_MEDIA_SCHEMA},
SERVICE_SET_SHUFFLE: {
'method': 'async_set_shuffle',
'schema': MEDIA_PLAYER_SET_SHUFFLE_SCHEMA},
SERVICE_CALL_METHOD: {
'method': 'async_call_method',
'schema': MEDIA_PLAYER_CALL_METHOD_SCHEMA},
}
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Kodi platform."""
if DATA_KODI not in hass.data:
hass.data[DATA_KODI] = []
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
tcp_port = config.get(CONF_TCP_PORT)
@@ -128,8 +138,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
host=host, port=port, tcp_port=tcp_port, encryption=encryption,
username=config.get(CONF_USERNAME),
password=config.get(CONF_PASSWORD),
turn_off_action=config.get(CONF_TURN_OFF_ACTION), websocket=websocket)
turn_off_action=config.get(CONF_TURN_OFF_ACTION),
timeout=config.get(CONF_TIMEOUT), websocket=websocket)
hass.data[DATA_KODI].append(entity)
async_add_devices([entity], update_before_add=True)
@asyncio.coroutine
@@ -141,23 +153,37 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
params = {key: value for key, value in service.data.items()
if key != 'entity_id'}
yield from getattr(entity, method['method'])(**params)
entity_ids = service.data.get('entity_id')
if entity_ids:
target_players = [player for player in hass.data[DATA_KODI]
if player.entity_id in entity_ids]
else:
target_players = hass.data[DATA_KODI]
update_tasks = []
if entity.should_poll:
update_coro = entity.async_update_ha_state(True)
update_tasks.append(update_coro)
for player in target_players:
yield from getattr(player, method['method'])(**params)
for player in target_players:
if player.should_poll:
update_coro = player.async_update_ha_state(True)
update_tasks.append(update_coro)
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
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(
os.path.dirname(__file__), 'services.yaml'))
for service in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[service].get(
'schema', MEDIA_PLAYER_SCHEMA)
schema = SERVICE_TO_METHOD[service]['schema']
hass.services.async_register(
DOMAIN, service, async_service_handler,
description=None, schema=schema)
description=descriptions.get(service), schema=schema)
def cmd(func):
@@ -185,7 +211,7 @@ class KodiDevice(MediaPlayerDevice):
def __init__(self, hass, name, host, port, tcp_port, encryption=False,
username=None, password=None, turn_off_action=None,
websocket=True):
timeout=DEFAULT_TIMEOUT, websocket=True):
"""Initialize the Kodi device."""
import jsonrpc_async
import jsonrpc_websocket
@@ -193,7 +219,7 @@ class KodiDevice(MediaPlayerDevice):
self._name = name
kwargs = {
'timeout': DEFAULT_TIMEOUT,
'timeout': timeout,
'session': async_get_clientsession(hass),
}
@@ -657,16 +683,40 @@ class KodiDevice(MediaPlayerDevice):
{"item": {"file": str(media_id)}})
@asyncio.coroutine
def async_set_shuffle(self, shuffle_on):
def async_set_shuffle(self, shuffle):
"""Set shuffle mode, for the first player."""
if len(self._players) < 1:
raise RuntimeError("Error: No active player.")
yield from self.server.Player.SetShuffle(
{"playerid": self._players[0]['playerid'], "shuffle": shuffle_on})
{"playerid": self._players[0]['playerid'], "shuffle": shuffle})
@asyncio.coroutine
def async_call_method(self, method, **kwargs):
"""Run Kodi JSONRPC API method with params."""
import jsonrpc_base
_LOGGER.debug('Run API method "%s", kwargs=%s', method, kwargs)
result_ok = False
try:
result = yield from getattr(self.server, method)(**kwargs)
result_ok = True
except jsonrpc_base.jsonrpc.ProtocolError as exc:
result = exc.args[2]['error']
_LOGGER.error('Run API method %s.%s(%s) error: %s',
self.entity_id, method, kwargs, result)
if isinstance(result, dict):
event_data = {'entity_id': self.entity_id,
'result': result,
'result_ok': result_ok,
'input': {'method': method, 'params': kwargs}}
_LOGGER.debug('EVENT kodi_call_method_result: %s', event_data)
self.hass.bus.async_fire(EVENT_KODI_CALL_METHOD_RESULT,
event_data=event_data)
return result
@asyncio.coroutine
def async_add_media_to_playlist(
self, media_type, media_id=None, media_name='', artist_name=''):
self, media_type, media_id=None, media_name='ALL', artist_name=''):
"""Add a media to default playlist (i.e. playlistid=0).
First the media type must be selected, then
@@ -675,13 +725,14 @@ class KodiDevice(MediaPlayerDevice):
All the albums of an artist can be added with
media_name="ALL"
"""
import jsonrpc_base
params = {"playlistid": 0}
if media_type == "SONG":
if media_id is None:
media_id = yield from self.async_find_song(
media_name, artist_name)
yield from self.server.Playlist.Add(
{"playlistid": 0, "item": {"songid": int(media_id)}})
if media_id:
params["item"] = {"songid": int(media_id)}
elif media_type == "ALBUM":
if media_id is None:
@@ -691,12 +742,22 @@ class KodiDevice(MediaPlayerDevice):
media_id = yield from self.async_find_album(
media_name, artist_name)
if media_id:
params["item"] = {"albumid": int(media_id)}
yield from self.server.Playlist.Add(
{"playlistid": 0, "item": {"albumid": int(media_id)}})
else:
raise RuntimeError("Unrecognized media type.")
if media_id is not None:
try:
yield from self.server.Playlist.Add(params)
except jsonrpc_base.jsonrpc.ProtocolError as exc:
result = exc.args[2]['error']
_LOGGER.error('Run API method %s.Playlist.Add(%s) error: %s',
self.entity_id, media_type, result)
else:
_LOGGER.warning('No media detected for Playlist.Add')
@asyncio.coroutine
def async_add_all_albums(self, artist_name):
"""Add all albums of an artist to default playlist (i.e. playlistid=0).
@@ -734,9 +795,13 @@ class KodiDevice(MediaPlayerDevice):
def async_find_artist(self, artist_name):
"""Find artist by name."""
artists = yield from self.async_get_artists()
out = self._find(
artist_name, [a['artist'] for a in artists['artists']])
return artists['artists'][out[0][0]]['artistid']
try:
out = self._find(
artist_name, [a['artist'] for a in artists['artists']])
return artists['artists'][out[0][0]]['artistid']
except KeyError:
_LOGGER.warning('No artists were found: %s', artist_name)
return None
@asyncio.coroutine
def async_get_songs(self, artist_id=None):
@@ -769,8 +834,14 @@ class KodiDevice(MediaPlayerDevice):
artist_id = yield from self.async_find_artist(artist_name)
albums = yield from self.async_get_albums(artist_id)
out = self._find(album_name, [a['label'] for a in albums['albums']])
return albums['albums'][out[0][0]]['albumid']
try:
out = self._find(
album_name, [a['label'] for a in albums['albums']])
return albums['albums'][out[0][0]]['albumid']
except KeyError:
_LOGGER.warning('No albums were found with artist: %s, album: %s',
artist_name, album_name)
return None
@staticmethod
def _find(key_word, words):
@@ -14,9 +14,7 @@ from homeassistant.components.media_player import (
from homeassistant.const import (STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['https://github.com/miracle2k/onkyo-eiscp/archive/'
'066023aec04770518d494c32fb72eea0ec5c1b7c.zip#'
'onkyo-eiscp==1.0']
REQUIREMENTS = ['onkyo-eiscp==1.1']
_LOGGER = logging.getLogger(__name__)
@@ -17,9 +17,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
import homeassistant.loader as loader
REQUIREMENTS = [
'https://github.com/bah2830/python-roku/archive/3.1.3.zip'
'#roku==3.1.3']
REQUIREMENTS = ['python-roku==3.1.3']
KNOWN_HOSTS = []
DEFAULT_PORT = 8060
@@ -15,9 +15,7 @@ from homeassistant.const import (
CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON, CONF_NAME)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = [
'https://github.com/laf/russound/archive/0.1.7.zip'
'#russound==0.1.7']
REQUIREMENTS = ['russound==0.1.7']
_LOGGER = logging.getLogger(__name__)
@@ -269,3 +269,34 @@ soundtouch_remove_zone_slave:
slaves:
description: Name of slaves entities to remove from the existing zone
example: 'media_player.soundtouch_bedroom'
kodi_add_to_playlist:
description: Add music to the default playlist (i.e. playlistid=0).
fields:
entity_id:
description: Name(s) of the Kodi entities where to add the media.
example: 'media_player.living_room_kodi'
media_type:
description: Media type identifier. It must be one of SONG or ALBUM.
example: ALBUM
media_id:
description: Unique Id of the media entry to add (`songid` or albumid`). If not defined, `media_name` and `artist_name` are needed to search the Kodi music library.
example: 123456
media_name:
description: Optional media name for filtering media. Can be 'ALL' when `media_type` is 'ALBUM' and `artist_name` is specified, to add all songs from one artist.
example: 'Highway to Hell'
artist_name:
description: Optional artist name for filtering media.
example: 'AC/DC'
kodi_call_method:
description: 'Call a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call will be redirected in a Home Assistant event: `kodi_call_method_result`.'
fields:
entity_id:
description: Name(s) of the Kodi entities where to run the API method.
example: 'media_player.living_room_kodi'
method:
description: Name of the Kodi JSONRPC API method to be called.
example: 'VideoLibrary.GetRecentlyAddedEpisodes'
+47 -2
View File
@@ -51,6 +51,7 @@ SERVICE_SNAPSHOT = 'sonos_snapshot'
SERVICE_RESTORE = 'sonos_restore'
SERVICE_SET_TIMER = 'sonos_set_sleep_timer'
SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer'
SERVICE_UPDATE_ALARM = 'sonos_update_alarm'
DATA_SONOS = 'sonos'
@@ -62,6 +63,11 @@ CONF_INTERFACE_ADDR = 'interface_addr'
# Service call validation schemas
ATTR_SLEEP_TIME = 'sleep_time'
ATTR_ALARM_ID = 'alarm_id'
ATTR_VOLUME = 'volume'
ATTR_ENABLED = 'enabled'
ATTR_INCLUDE_LINKED_ZONES = 'include_linked_zones'
ATTR_TIME = 'time'
ATTR_MASTER = 'master'
ATTR_WITH_GROUP = 'with_group'
@@ -90,6 +96,14 @@ SONOS_SET_TIMER_SCHEMA = SONOS_SCHEMA.extend({
vol.All(vol.Coerce(int), vol.Range(min=0, max=86399))
})
SONOS_UPDATE_ALARM_SCHEMA = SONOS_SCHEMA.extend({
vol.Required(ATTR_ALARM_ID): cv.positive_int,
vol.Optional(ATTR_TIME): cv.time,
vol.Optional(ATTR_VOLUME): cv.small_float,
vol.Optional(ATTR_ENABLED): cv.boolean,
vol.Optional(ATTR_INCLUDE_LINKED_ZONES): cv.boolean,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Sonos platform."""
@@ -163,9 +177,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
elif service.service == SERVICE_RESTORE:
device.restore(service.data[ATTR_WITH_GROUP])
elif service.service == SERVICE_SET_TIMER:
device.set_timer(service.data[ATTR_SLEEP_TIME])
device.set_sleep_timer(service.data[ATTR_SLEEP_TIME])
elif service.service == SERVICE_CLEAR_TIMER:
device.clear_timer()
device.clear_sleep_timer()
elif service.service == SERVICE_UPDATE_ALARM:
device.update_alarm(**service.data)
device.schedule_update_ha_state(True)
@@ -193,6 +209,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
DOMAIN, SERVICE_CLEAR_TIMER, service_handle,
descriptions.get(SERVICE_CLEAR_TIMER), schema=SONOS_SCHEMA)
hass.services.register(
DOMAIN, SERVICE_UPDATE_ALARM, service_handle,
descriptions.get(SERVICE_UPDATE_ALARM),
schema=SONOS_UPDATE_ALARM_SCHEMA)
def _parse_timespan(timespan):
"""Parse a time-span into number of seconds."""
@@ -1034,6 +1055,30 @@ class SonosDevice(MediaPlayerDevice):
"""Clear the timer on the player."""
self._player.set_sleep_timer(None)
@soco_error
@soco_coordinator
def update_alarm(self, **data):
"""Set the alarm clock on the player."""
from soco import alarms
a = None
for alarm in alarms.get_alarms(self.soco):
# pylint: disable=protected-access
if alarm._alarm_id == str(data[ATTR_ALARM_ID]):
a = alarm
if a is None:
_LOGGER.warning("did not find alarm with id %s",
data[ATTR_ALARM_ID])
return
if ATTR_TIME in data:
a.start_time = data[ATTR_TIME]
if ATTR_VOLUME in data:
a.volume = int(data[ATTR_VOLUME] * 100)
if ATTR_ENABLED in data:
a.enabled = data[ATTR_ENABLED]
if ATTR_INCLUDE_LINKED_ZONES in data:
a.include_linked_zones = data[ATTR_INCLUDE_LINKED_ZONES]
a.save()
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
@@ -176,6 +176,7 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
self._state = STATE_PAUSED
if current.get('is_playing'):
self._state = STATE_PLAYING
self._shuffle = current.get('shuffle_state')
device = current.get('device')
if device is None:
self._state = STATE_IDLE
@@ -184,8 +185,6 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
self._volume = device.get('volume_percent') / 100
if device.get('name'):
self._current_device = device.get('name')
if device.get('shuffle_state'):
self._shuffle = device.get('shuffle_state')
def set_volume_level(self, volume):
"""Set the volume level."""
@@ -213,7 +212,8 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
def select_source(self, source):
"""Select playback device."""
self._player.transfer_playback(self._devices[source])
self._player.transfer_playback(self._devices[source],
self._state == STATE_PLAYING)
def play_media(self, media_type, media_id, **kwargs):
"""Play media."""
+8 -3
View File
@@ -28,10 +28,12 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'microsoft_face'
DEPENDENCIES = ['camera']
FACE_API_URL = "https://westus.api.cognitive.microsoft.com/face/v1.0/{0}"
FACE_API_URL = "api.cognitive.microsoft.com/face/v1.0/{0}"
DATA_MICROSOFT_FACE = 'microsoft_face'
CONF_AZURE_REGION = 'azure_region'
SERVICE_CREATE_GROUP = 'create_group'
SERVICE_DELETE_GROUP = 'delete_group'
SERVICE_TRAIN_GROUP = 'train_group'
@@ -49,6 +51,7 @@ DEFAULT_TIMEOUT = 10
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_AZURE_REGION, default="westus"): cv.string,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
}),
}, extra=vol.ALLOW_EXTRA)
@@ -115,6 +118,7 @@ def async_setup(hass, config):
entities = {}
face = MicrosoftFace(
hass,
config[DOMAIN].get(CONF_AZURE_REGION),
config[DOMAIN].get(CONF_API_KEY),
config[DOMAIN].get(CONF_TIMEOUT),
entities
@@ -304,12 +308,13 @@ class MicrosoftFaceGroupEntity(Entity):
class MicrosoftFace(object):
"""Microsoft Face api for HomeAssistant."""
def __init__(self, hass, api_key, timeout, entities):
def __init__(self, hass, server_loc, api_key, timeout, entities):
"""Initialize Microsoft Face api."""
self.hass = hass
self.websession = async_get_clientsession(hass)
self.timeout = timeout
self._api_key = api_key
self._server_url = "https://{0}.{1}".format(server_loc, FACE_API_URL)
self._store = {}
self._entities = entities
@@ -346,7 +351,7 @@ class MicrosoftFace(object):
params=None):
"""Make a api call."""
headers = {"Ocp-Apim-Subscription-Key": self._api_key}
url = FACE_API_URL.format(function)
url = self._server_url.format(function)
payload = None
if binary:
+1 -2
View File
@@ -25,8 +25,7 @@ from homeassistant.components.http import HomeAssistantView
from homeassistant.components.frontend import add_manifest_json_key
from homeassistant.helpers import config_validation as cv
# pyelliptic is dependency of pywebpush and 1.5.8 contains a breaking change
REQUIREMENTS = ['pywebpush==0.6.1', 'PyJWT==1.4.2', 'pyelliptic==1.5.7']
REQUIREMENTS = ['pywebpush==1.0.0', 'PyJWT==1.4.2']
DEPENDENCIES = ['frontend']
+1 -1
View File
@@ -13,7 +13,7 @@ from homeassistant.components.notify import (
from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['sendgrid==4.0.0']
REQUIREMENTS = ['sendgrid==4.1.0']
_LOGGER = logging.getLogger(__name__)
+48 -7
View File
@@ -9,9 +9,9 @@ import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
import email.utils
from email.mime.application import MIMEApplication
import email.utils
import os
import voluptuous as vol
from homeassistant.components.notify import (
@@ -26,10 +26,12 @@ import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
ATTR_IMAGES = 'images' # optional embedded image file attachments
ATTR_HTML = 'html'
CONF_STARTTLS = 'starttls'
CONF_DEBUG = 'debug'
CONF_SERVER = 'server'
CONF_SENDER_NAME = 'sender_name'
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 25
@@ -47,6 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_STARTTLS, default=DEFAULT_STARTTLS): cv.boolean,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SENDER_NAME): cv.string,
vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): cv.boolean,
})
@@ -62,6 +65,7 @@ def get_service(hass, config, discovery_info=None):
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
config.get(CONF_RECIPIENT),
config.get(CONF_SENDER_NAME),
config.get(CONF_DEBUG))
if mail_service.connection_is_valid():
@@ -74,7 +78,7 @@ class MailNotificationService(BaseNotificationService):
"""Implement the notification service for E-Mail messages."""
def __init__(self, server, port, timeout, sender, starttls, username,
password, recipients, debug):
password, recipients, sender_name, debug):
"""Initialize the service."""
self._server = server
self._port = port
@@ -84,6 +88,8 @@ class MailNotificationService(BaseNotificationService):
self.username = username
self.password = password
self.recipients = recipients
self._sender_name = sender_name
self._timeout = timeout
self.debug = debug
self.tries = 2
@@ -128,19 +134,28 @@ class MailNotificationService(BaseNotificationService):
Build and send a message to a user.
Will send plain text normally, or will build a multipart HTML message
with inline image attachments if images config is defined.
with inline image attachments if images config is defined, or will
build a multipart HTML if html config is defined.
"""
subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
data = kwargs.get(ATTR_DATA)
if data:
msg = _build_multipart_msg(message, images=data.get(ATTR_IMAGES))
if ATTR_HTML in data:
msg = _build_html_msg(message, data[ATTR_HTML],
images=data.get(ATTR_IMAGES))
else:
msg = _build_multipart_msg(message,
images=data.get(ATTR_IMAGES))
else:
msg = _build_text_msg(message)
msg['Subject'] = subject
msg['To'] = ','.join(self.recipients)
msg['From'] = self._sender
if self._sender_name:
msg['From'] = '{} <{}>'.format(self._sender_name, self._sender)
else:
msg['From'] = self._sender
msg['X-Mailer'] = 'HomeAssistant'
msg['Date'] = email.utils.format_datetime(dt_util.now())
msg['Message-Id'] = email.utils.make_msgid()
@@ -155,12 +170,16 @@ class MailNotificationService(BaseNotificationService):
mail.sendmail(self._sender, self.recipients,
msg.as_string())
break
except smtplib.SMTPServerDisconnected:
_LOGGER.warning(
"SMTPServerDisconnected sending mail: retrying connection")
mail.quit()
mail = self.connect()
except smtplib.SMTPException:
_LOGGER.warning(
"SMTPException sending mail: retrying connection")
mail.quit()
mail = self.connect()
mail.quit()
@@ -204,3 +223,25 @@ def _build_multipart_msg(message, images):
body_html = MIMEText(''.join(body_text), 'html')
msg_alt.attach(body_html)
return msg
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")
msg = MIMEMultipart('related')
alternative = MIMEMultipart('alternative')
alternative.attach(MIMEText(text, _charset='utf-8'))
alternative.attach(MIMEText(html, ATTR_HTML, _charset='utf-8'))
msg.attach(alternative)
for atch_num, atch_name in enumerate(images):
name = os.path.basename(atch_name)
try:
with open(atch_name, 'rb') as attachment_file:
attachment = MIMEImage(attachment_file.read(), filename=name)
msg.attach(attachment)
attachment.add_header('Content-ID', '<{}>'.format(name))
except FileNotFoundError:
_LOGGER.warning('Attachment %s [#%s] not found. Skipping',
atch_name, atch_num)
return msg

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