Compare commits

..

261 Commits

Author SHA1 Message Date
Fabian Affolter fc442361cd Bump release to 0.61.0 2018-01-15 13:27:54 +01:00
Fabian Affolter c3ff5de016 Merge pull request #11589 from home-assistant/release-0-61
0.61
2018-01-15 10:35:00 +01:00
Alok Saboo 9b15cfa5a5 Update Pyarlo to 0.1.2 (#11626) 2018-01-15 09:38:08 +01:00
Jesse Hills 7837b4893f Use kelvin/mireds correctly for setting iglo white (#11622)
* Use kelvin/mireds correctly for setting iglo white

* Update requirements_all.txt

* Fix line lengths
2018-01-15 09:38:07 +01:00
Thijs de Jong d88edb0661 patch stop command (#11612) 2018-01-15 09:38:07 +01:00
Fabian Affolter 7a50450b92 Upgrade yarl to 0.18.0 (#11609) 2018-01-15 09:38:06 +01:00
Daniel Høyer Iversen 3afa4726bf Xiaomi lib upgrade (#11603)
* upgrade xiaomi lib

* xiaomi lib
2018-01-15 09:38:06 +01:00
Daniel Høyer Iversen 218e97d965 Bugfix and cleanup for Rfxtrx (#11600)
* rfxtrx clean up

* rfxtrx clean up

* rfxtrx clean up
2018-01-15 09:38:05 +01:00
Bob Anderson 8d19192c9c Concord232 alarm arm away fix (#11597)
* fix arming away cmd for concord232 client

* bump required version of concord232 to 0.15
2018-01-15 09:38:05 +01:00
tschmidty69 179d99381d Snips add say and say_actions services (new) (#11596)
* Added snips.say and snips.say_action services

* Added snips.say and snips.say_action services

* Merged services.yaml changes I missed

* added tests for new service configs

* Woof

* Woof Woof

* Changed attribute names to follow hass standards.

* updated test_snips with new attribute names
2018-01-15 09:38:04 +01:00
Adam Mills b8dfa4c3d2 Fix state for trigger with forced updates (#11595) 2018-01-15 09:38:03 +01:00
tschmidty69 92bec562ab Pushbullet email support (fix) (#11590)
* Simplified push calls

* Cleaned up and added unittests

* Fixed email parameter

* Fixed email parameter
2018-01-15 09:38:03 +01:00
Thijs de Jong 1802c0a922 Fix Tahoma stop command for 2 types of shutters (#11588)
* add working stop command

This fixes the stop command for 2 types of roller shutters

* fix line too long

* fix indentation

* fix indentation
2018-01-15 09:38:02 +01:00
Pascal Vizeli f2cc00cc64 Core support for hass.io calls & Bugfix check_config (#11571)
* Initial overwrites

* Add check_config function.

* Update hassio.py

* Address comments

* add hassio support

* add more tests

* revert core changes

* Address check_config

* Address comment with api_bool

* Bugfix check_config

* Update core.py

* Update test_core.py

* Update config.py

* Update hassio.py

* Update config.py

* Update test_config.py
2018-01-15 09:38:02 +01:00
Jack Fan 033e06868e Avoid returning empty media_image_url string (#11557)
This relates to issue
https://github.com/home-assistant/home-assistant/issues/11556
2018-01-15 09:37:57 +01:00
Paulus Schoutsen 4883036789 Update frontend 2018-01-11 15:50:50 -08:00
Andreas Björshammar 965bd8a2e0 Added support for enable/disable motion detection (#11583)
* Added support for enable/disable motion detection

* Changed name of variable for exception, lint error

* Moved motion_detection_enabled property to the other properties
2018-01-11 15:44:23 -08:00
Thijs de Jong 2224d05607 add velux roller shutter to tahoma (#11586)
With #11538 I forgot to add another type of Roller shutters that should be supported.
2018-01-11 15:43:31 -08:00
cgtobi ed66c21035 Cast return values and add unit tests for the yahoo weather component. (#10699)
* Add unittest for yahoo weather component.

* Add requitements for tests.

* Update requirements.

* Revert "Update requirements."

This reverts commit 024b5cf8bf.

* Update requirements.

* Randomize sample data.

* Remove unnecessary methods.

* Remove dependency and replace with MockDependency.

* Add temporary fix for yahoo weather API issue.
2018-01-11 15:42:51 -08:00
Paulus Schoutsen 9384e2c963 Pr/11430 (#11587)
* Fix error when name is non-latin script

When the name is non-latin script (e.g. Japanese), slugify returns empty string and causes failure in a later stage. This commit fixes the issue by using default name.

* Add test
2018-01-11 15:26:48 -08:00
Bob Anderson 6af42b4372 Control ordering of display in history component (#11340)
* Make the order shown in the history component match the ordering given in the configuration of included entities (if any)

* return the sorted result

* optimize sorting.  since entities only appear once, we can break from a search on first find, and no copy of the list is needed
2018-01-11 15:21:23 -08:00
Laqoore 1235dae5b8 Changed device type of media player and cover to switch (#11483)
* Changed device type of media player and cover to switch

Covers and media players should not be of device type 'light'. Example: If user requests all lights to switch to off, covers are closed and media players are affected too.

* Fix test
2018-01-11 15:01:38 -08:00
Andrey a35d05ac7e Remove aux_heat support from Sensibo now that UI supports on/off (#11579) 2018-01-11 23:25:15 +01:00
kennedyshead 6b103cc9b9 Fix for asuswrt, telnet not working and presence-detection for router mode (#11422)
* Fix for telnet mode
Fix for presence detection

closes #11349 #10231

* Optimisation when device is disconnected
2018-01-11 14:12:27 -08:00
Sean Wilson a23f60315f Fix bluetooth tracker source (#11469)
* Add new sources for bluetooth and bluetooth_le trackers (rather than just using the generic 'gps' catch-all).

* Fix lint issues
2018-01-11 14:10:47 -08:00
Adam Mills 3bc6a7da32 Support OSRAM lights on ZHA (#11522) 2018-01-11 13:56:00 -08:00
hawk259 dc5b610ee5 Alarmdecoder add validation of the zone types (#11488)
* Alarmdecoder add validation of the zone types

* fix line length
2018-01-11 13:53:14 -08:00
Daniel Perna 7723db9e62 Update pyhomematic, support new devices (#11578) 2018-01-11 20:08:09 +01:00
Ulrich Dobramysl cf612c3d5b Make the rpi_rf component thread-safe using an RLock (#11487)
* Make the rpi_rf component thread-safe

The previous implementation suffered from race conditions when two rpi_rf switches are triggered at the same time. This implementation uses an RLock to give one thread at a time exclusive access to the rfdevice object.

* cleanup

* fix lint
2018-01-11 13:47:05 +01:00
Josh Anderson d2b6660881 Tado improvements - hot water zone sensors and climate precision (#11521)
* Add tado hot water zone sensors

* Set precision to match tado app/website
2018-01-11 10:49:41 +01:00
Dan Nixon 3972d1d4c6 Mark REST binary_sensor unavailable if request fails (#11506)
* Mark REST binary_sensor unavailable if request fails

* Add test suite for RESTful binary sensor
2018-01-11 10:48:15 +01:00
Anders Melchiorsen 5fda78cf91 Fix new squeezebox service descriptions for lazy loading (#11574) 2018-01-11 10:15:59 +01:00
randellhodges 60ce2b343d Tracking all groups to allow changing of existing groups (#11444)
* Tracking all groups to allow changing of existing groups

* Unit tests

* Fix flake8 warnings in test
2018-01-10 14:13:22 -08:00
Andrey 6cc285aea5 Add sensibo_assume_state service to Sensibo climate (#11567) 2018-01-10 14:04:35 -08:00
Pascal Vizeli c5d5d57e9b Extend hass.io services / updater (#11549)
* Extend hass.io services

* Add warning for carfuly options with hass.io

* update tests

* finish tests

* remove update calls

* address comments

* address comments p2

* fix tests

* fix tests

* Use token also for proxy

* Add test for server_host

* Fix test

* Fix tests

* Add test for version

* Address comments
2018-01-10 19:48:31 +01:00
Thijs de Jong 02979db3d6 Add Velux Windows to Tahoma (#11538)
* Add Velux Windows to Tahoma

* fix linit

* add supported Tahoma devices by @glpatcern

* hound

* lint

* fix logging

* lint

* lint

* remove blank line after docstring

* changes based on notes by @armills

* fix logging
2018-01-10 09:41:16 -05:00
Paulus Schoutsen 0d06e8c1c9 Test tweaks (#11560)
* Fix is_allowed_path on OS X

* Move APNS2 inside func in test
2018-01-10 10:48:17 +01:00
Paulus Schoutsen d793cfeb68 Update frontend to 20180110.0 2018-01-10 00:52:35 -08:00
Paulus Schoutsen 382a62346b Update frontend to 20180110.0 2018-01-10 00:52:12 -08:00
Anders Melchiorsen 9e0ca719ed Deprecate explicit entity_id in template platforms (#11123)
* Deprecate explicit entity_id in template platforms

* Use config validator for deprecation

* Fix copy/paste typos

* Also print the config value

* Add test for config validator

* Assert the module name that logged the message
2018-01-10 00:06:26 -08:00
Paulus Schoutsen 88b70e964c Remove execution file perm (#11563) 2018-01-09 23:55:14 -08:00
Andrea Campi 4dda842b16 Try to fix crashes after Hue refactoring (#11270)
* Try to fix crashes after Hue refactoring

Refs #11183

* Fix usage of dispatcher_send via helper.

* Address review feedback.
2018-01-10 08:05:04 +01:00
Nolan Gilley 3cba09c6f6 Coinbase.com sensor platform (#11036)
* coinbase sensors

use hass.data, load_platform

* add exchange rate sensors

dont pass complex object over event bus

* check for auth error
2018-01-10 07:47:22 +01:00
Fabian Affolter c4bc42d527 Upgrade keyring to 10.3.2 (#11531) 2018-01-09 19:51:35 -08:00
Phil Kates cf04a81f70 Fix error on entity_config missing (#11561)
If the `google_assistant` key exists in the config but has no
`entity_config` key under it you'll get an error.

```
  File "/Users/pkates/src/home-assistant/homeassistant/components/google_assistant/http.py", line 51, in is_exposed
    entity_config.get(entity.entity_id, {}).get(CONF_EXPOSE)
AttributeError: 'NoneType' object has no attribute 'get'
```
2018-01-09 19:47:24 -08:00
Eric Pignet 92014bf1d1 Add 2 media_player services and 1 custom service to Squeezebox platform (#10969)
* Add 2 media_player services and 1 custom service to Squeezebox platform

* Fix pylint error

* Remove apostrophe in example

* Split method into command and parameters

* Fix Lint error
2018-01-09 22:05:21 -05:00
cdce8p cba55402b1 Improved test runtime (#11553)
* Remove redundent assert statements and cleanup

* Added 'get_date' function

* Replace 'freeze_time' with 'mock.patch'
* Tox in 185s (py35)

* Removed obsolete 'freeze_time' from test_updater
* Tox 162s (py35)

* Remove test requirement 'freezegun'

* Fixed flake8 errors

* Added 'mock.patch' for 'feedparser.parse'

* Made 'FUNCTION_PATH' a constant

* Remove debug statements.
2018-01-09 16:00:49 -08:00
Fabian Affolter f56b3d8e9c Upgrade lightify to 1.0.6.1 (#11545) 2018-01-09 15:35:34 -08:00
Paulus Schoutsen 8313225b40 Move Google Assistant entity config out of customize (#11499)
* Move Google Assistant entity config out of customize

* CONF_ALIAS -> CONF_ALIASES

* Lint
2018-01-09 15:14:56 -08:00
Sean Wilson 13042d5557 ZoneMinder event sensor updates (#11369)
* Switch the ZoneMinder event sensor over to use the consoleEvents API, and add monitored_conditions for 'hour', 'day', 'week', 'month' and 'all'. 'all' is enabled by default to provide backward compatibility with the old behaviour.

* - Switch to new string formatting
- Remove redundant validator

* De-lint
2018-01-09 21:58:26 +01:00
Fabian Affolter 85858885c8 Upgrade Sphinx to 1.6.6 (#11534) 2018-01-09 15:30:54 +01:00
Fabian Affolter 10f48fbf6b Upgrade python-etherscan-api to 0.0.2 (#11535) 2018-01-09 15:30:36 +01:00
Fabian Affolter 8b267e3faf Upgrade numpy to 1.14.0 (#11542) 2018-01-09 15:30:00 +01:00
tschmidty69 13effee679 Snips: (fix) support new intentName format (#11509)
* support new intentName format

* Added tests for new and old format names

* pylint warning fixes
2018-01-08 13:00:21 -08:00
Cameron Llewellyn 903cda08b1 Insteon local update (#11088)
* trying to rework device discovery. now the main component will do the getlinked and pass it to the sub-components. no longer any config needed other than what is needed to connect to the hub. device names are no longer stored. core team told us to stop using configurator to ask for names. there should be a way to set names in hass...possibly this https://home-assistant.io/docs/configuration/customizing-devices/

* fix device types

* make device names just be the isnteon device id

* revert some config changes

* Update insteon_local.py

* Update insteon_local.py

* Update insteon_local.py

* Update insteon_local.py

* Update insteon_local.py

* Update insteon_local.py

* Update insteon_local.py

* update insteon client

* linting fixes

* Error Clean up

* Update to make requested changes

* more changes

* Finish requested changes to components

* Fixing Rebase Conflicts

* fix device types

* make device names just be the isnteon device id

* revert some config changes

* Update insteon_local.py

* Update insteon_local.py

* Update insteon_local.py

* Update insteon_local.py

* Update insteon_local.py

* Update insteon_local.py

* Update insteon_local.py

* update insteon client

* linting fixes

* Error Clean up

* Update to make requested changes

* more changes

* Finish requested changes to components

* Update Insteon_Local for performance improvements

* Fix errors from get_linked

* Fix typo

* Requested changes

* Fix spacing

* Clean up

* Requested Changes
2018-01-08 18:18:10 +01:00
Yien Xu e0e2f739ba Add options feature to Baidu TTS. (#11462)
* Add options feature to Baidu TTS.

Add options feature: supported_options() and default_options() added, get_tts_audio updated to accommodate options.

* Add options feature to Baidu TTS.

Add options feature: supported_options() and default_options() added, get_tts_audio updated to accommodate options.

* Fix style

* Fix style

Change the order of content of lists and dictionaries.

* Fix style

Changed order of imports, and fixed grammar errors.
2018-01-08 17:22:50 +01:00
Michael Kutý 0e710099e0 Support pushing all sensors and fix wrong metrics. (#11159)
For example all metrics with unit % match humidity.
This generate correct metrics like this
# HELP nut_ups_battery_charge sensor.nut_ups_battery_charge
# TYPE nut_ups_battery_charge gauge
nut_ups_battery_charge{entity="sensor.nut_ups_battery_charge",friendly_name="NUT UPS Battery Charge"} 98.0
nut_ups_battery_charge{entity="sensor.nut_ups_battery_charge_2",friendly_name="NUT UPS Battery Charge"} 97.0
2018-01-08 17:11:45 +01:00
timstanley1985 b1b0a2589e MQTT json attributes (#11439)
* MQTT json attributes

* Fix lint

* Amends following comments

* Fix lint

* Fix lint

* Add test

* Fix typo

* Amends following comments

* New tests

* Fix lint

* Fix tests
2018-01-08 17:07:39 +01:00
ChristianKuehnel dff36b8087 Extension sensor alpha_vantage (#11427)
* Extended sensor alpha_vantage

* Improved error handling if symbol is not found. Now we add the symbols that were found,
* Added option to give names and currencies to symbols.
* Added support to read foreign exchange information.
* Icons are selected based on the currency (where MDI has a matching icon)

* added missing line at the end of the file...

* renamed variable as required by pylint

* Fix typos, ordering, and style
2018-01-08 16:58:34 +01:00
florianj1 46ad194097 Fix Kodi channels media type (#11505)
* Update kodi.py

* Update kodi.py

Fix code style
2018-01-08 16:57:50 +01:00
Alex Osadchyy 2ba9d825a0 Reconnect before mochad switch send command (#11296)
* Connection to mochad occasionally stalls on RPi and CM19A. Reconnect one switch send command.

* Formatting and exception hanling fixes

* Moved import inside the method. Logging outside the try-catch.

* Tailing whitespaces.

* MockDependency on pymochad in unit tests to resolve exceptions

* patch pymochad MochadException in unit tests to resolve exceptions

* patch pymochad MochadException in unit tests to resolve exceptions

* cleaned unused import

* lint issue fixed

* pylint issue fixed
2018-01-08 08:32:24 +01:00
Chris Cowart 8c0035c5b3 New features for Owntracks device_tracker (#11480)
* New features for Owntracks device_tracker

- Supporting a mapping of region names in OT to zones in HA, allowing
  separate namespaces in both applications. This is especially helpful
  if using one OT instance to update geofences for multiple homes.
- Creating a setting to ignore all location updates, allowing users to
  rely completely on enter and leave events. I have personally always
  used OT integrations with home automation this way and find it the
  most reliable.
- Allowing the OT topic to be overridden in configuration

* Fixing configuration of MQTT topic, related tests

* Tests for Owntracks events_only feature

* Tests for customizing mqtt topic, region mapping

* Fixing _parse and http for owntracks custom topic

* Making tests more thorough and cleaning up lint
2018-01-08 08:16:45 +01:00
Dan Nixon c53fc94e84 Address missed review comments for Dark Sky weather (#11520)
See #11435
2018-01-07 19:54:27 -08:00
Paulus Schoutsen bccd880395 Fix canary flaky test (#11519) 2018-01-07 15:05:19 -08:00
Anders Melchiorsen 8267a21bfe Lazy loading of service descriptions (#11479)
* Lazy loading of service descriptions

* Fix tests

* Load YAML in executor

* Return a copy of available services to allow mutations

* Remove lint

* Add zha/services.yaml

* Only cache descriptions for known services

* Remove lint

* Remove description loading during service registration

* Remove description parameter from async_register

* Test async_get_all_descriptions

* Remove lint

* Fix typos from multi-edit

* Remove unused arguments

* Remove unused import os

* Remove unused import os, part 2

* Remove unneeded coroutine decorator

* Only use executor for loading files

* Cleanups suggested in review

* Increase test coverage

* Fix races in existing tests
2018-01-07 14:54:16 -08:00
Dan Nixon 3cbd77f6ac Add Dark Sky weather component (#11435) 2018-01-07 13:59:32 -08:00
tschmidty69 4a6b5ba02b Snips (new) added speech response, parse snips/duration (#11513)
* Snips changed to using <username>:intentName instead of user_IDSTRING__intentName

* added response to snips

* blank line

* added unittests

* houndy

* sdtuff

* more stuff

* Update test_snips.py

* Update snips.py

* Split log tests to avoid dict ordering in py34

* Update test_snips.py

* Update test_snips.py

* still broken

* fixed tests

* fixed tests

* removed fix submitted in another PR
2018-01-07 13:42:08 -08:00
Julius Mittenzwei efb83dde19 More tolerant KNX component if gateway cant be connected (#11511)
* Issue #11432: Do not stop initializing KNX when tunelling device cant be reached

*  Issue #11432: Mark devices as unavailable if gateway cant be connected
2018-01-07 13:39:14 -08:00
Julio Guerra c20324793e timer: include the remaining time in the state attributes (#11510)
Add the amount of remaining time before a timer is finished in its state attributes, so that it is received when fetching a timer state.

In my particular case, it allows me to correctly implement a UI for a timer when it is paused and when the UI didn't received the pause state change (which happens when the UI is started after the pause). In this case, it is impossible to show the remaining amount of time before the timer ends.
2018-01-07 13:37:19 -08:00
Julian Kahnert 4496ee5af0 upgrade schiene to 0.20 (#11504) 2018-01-07 14:57:26 +01:00
Christopher Viel e42c4859c2 Upgrade pywebpush to 1.5.0 (#11497)
This version includes a fix for the serialization errors that occurred
when updating push subscriptions.

Changes since version 1.3.0:
https://github.com/web-push-libs/pywebpush/compare/1.3.0...28d2b55f
2018-01-06 21:08:11 -08:00
Tom Waters 939d1b5ff6 Fix time functions would throw errors in python scripts (#11414)
* Fix time functions would throw errors in python scripts

* Added unit test for time.strptime, change variable name to satisfy lint

* Added docstring for time attribute wrapper method to satisfy lint

* Fixed line too long lint problem
2018-01-07 01:50:55 +01:00
Paulus Schoutsen 5c2cecde70 Clean up Alexa.intent and DialogFlow.intent (#11492)
* Clean up Alexa.intent

* Restructure dialogflow too

* Lint

* Lint
2018-01-07 01:39:32 +01:00
Fabian Affolter c613585100 Add missing configuration variables (#11390)
* Add missing configuration variables

* Minor changes

* Sync platforms and other minor updates
2018-01-06 21:53:14 +01:00
Jesse Hills 3fbf09e7d9 Add new iGlo component (#11171)
* Add new iGlo component

* Missing comma
Add extra blank lines

* Dont change state in turn_on
Remove unused variables
Update before add

* Fixing some lint issues
2018-01-06 21:52:31 +01:00
Fabian Affolter b22a26891e Upgrade pysnmp to 4.4.4 (#11485) 2018-01-06 19:54:15 +01:00
abondoe a0df165011 Add Touchline climate platform (#10547)
* Add toucline platform

* Fix bugs suggested by houndci-bot

* Fix bugs suggested by houndci-bot

* Fix style based on comments and lint

* Remove target temperature

* Fix unexpected EOF

* Fix unexpected EOF

* Fix wrongfully named numberOfDevices variable

* Add target temperature

* Update requirements_all.txt

* Change after review

* Add supported features
* Remove update in constructor
* Set member variables to none in constructor

* Minor changes
2018-01-06 11:23:24 +01:00
Fabian Affolter f6307a1523 Upgrade yarl to 0.17.0 (#11478) 2018-01-06 09:42:09 +01:00
Paulus Schoutsen ff9688bb7a Fix vultr tests (#11477) 2018-01-05 16:34:03 -08:00
Fabian Affolter c714658384 Upgrade alpha_vantage to 1.8.0 (#11476) 2018-01-05 16:05:51 -08:00
Fabian Affolter f21914d1f3 Upgrade psutil to 5.4.3 (#11468) 2018-01-05 14:30:12 -08:00
hawk259 417c193c0d AlarmDecoder remove icon function as BinarySensorDevice handles it correctly now (#11467)
* remove icon function as BinarySensorDevice handles it correctly now

* removing _type, not used
2018-01-05 14:29:57 -08:00
Christopher Viel 455c629f47 Don't duplicate html5 registrations (#11451)
* Don't duplicate html5 registrations

If a registration is posted and another registration with the same
endpoint URL exists, update that one instead. That way, we preserve
the device name that has been configured. The previous behavior used to
append 'unnamed device' registrations over and over, leading to
multiple copies of the same registration. The endpoint URL is unique per
service worker so it is safe to update matching registrations.

* Refactor html5 registration view to not write json in the event loop
2018-01-05 14:29:27 -08:00
Paulus Schoutsen 01896786fb Merge branch 'master' into dev 2018-01-05 13:10:29 -08:00
Paulus Schoutsen c526fcd40f Merge pull request #11470 from home-assistant/release-0-60-1
0.60.1
2018-01-05 12:55:13 -08:00
Paulus Schoutsen 8b57777ce9 Alexa to not use customize for entity config (#11461)
* Alexa to not use customize for entity config

* Test Alexa entity config

* Improve tests

* Fix test
2018-01-05 12:33:22 -08:00
Matt N b61197196e iOS 10 should be served javascript_version:es5 (#11387)
* iOS 10 should be served javascript_version:es5

Fixes #11234

* Update min Safari version to 12
2018-01-05 11:29:20 -08:00
Pascal Vizeli e0a1b87296 Don't block on sevice call for alexa (#11358)
* Don't block on sevice call for alexa

* fix tests
2018-01-05 11:29:19 -08:00
Greg Laabs d3ed6d5242 Fix leak sensors always showing Unknown until Wet (#11313)
Leak sensors were using the "wet" node as a negative node, which prevented them from ever gettng a Dry status unless the user pressed the button on the hardware after every Hass reboot.

This change ignores the Wet node, as it is just always the exact inverse of the Dry node. We don't need to watch both.
2018-01-05 11:29:19 -08:00
Bob Anderson 5d6455415e Fix unpredictable entity names in concord232 binary_sensor (#11292) 2018-01-05 11:29:18 -08:00
maxlaverse a27b37be59 Fix allday events in custom_calendars (#11272) 2018-01-05 11:29:18 -08:00
Zio Tibia f6a67d99e4 Fix handling zero values for state_on/state_off (#11264) 2018-01-05 11:29:17 -08:00
CTLS 486263fff7 Fix inverted sensors on the concord232 binary sensor component (#11261)
* Fix inverted sensors on the concord232 binary sensor component

* Changed from == Tripped to != Normal
2018-01-05 11:29:17 -08:00
Andrea Campi 5a469f4d4b Support multiple Hue bridges with lights of the same id (#11259)
* Improve support for multiple Hue bridges with lights that have the same id.

The old code pre-refactoring kept a per-bridge list of lights in a closure; my refactoring moved that to hass.data, which is convenient but caused them to conflict with each other.

Fixes #11183

* Update test_hue.py
2018-01-05 11:29:16 -08:00
Pascal Vizeli 889eef78e4 Bugfix homematic available modus (#11256) 2018-01-05 11:29:16 -08:00
Greg Laabs 794cfb7976 Fix detection of if a negative node is in use (#11255)
* Fix detection of if a negative node is in use

Fix a problem where every negative node gets detected as in-use. Code was not checking the correct property.

* Allow protected access
2018-01-05 11:29:15 -08:00
Daniel Watkins 9055922153 Fix webostv select source (#11227)
* Fix reuse of variable name

This should fix #11224.

* Add tests for LgWebOSDevice.select_source
2018-01-05 11:29:15 -08:00
Janne Grunau 2330268842 homematic: add username and password to interface config schema (#11214)
Fixes #11191, the json-rpc name resolving method requires user account
and password.
2018-01-05 11:29:15 -08:00
maxlaverse e81d17dae0 Fix webdav calendar schema (#11185) 2018-01-05 11:29:14 -08:00
Ben Randall 5820bab9b1 Fix async IO in Sesame lock component. (#11054)
* Call update on Sesame devices to cache initial state

* Switch to using async_add_devices

* Fix line length

* Fix Lint errors

* Fix more Lint errors

* Cache pysesame properties

* Updates from CR feedback
2018-01-05 11:29:14 -08:00
Paulus Schoutsen 3a00c3de64 Version bump to 0.60.1 2018-01-05 11:28:39 -08:00
Matt N 71fb7a6ef6 iOS 10 should be served javascript_version:es5 (#11387)
* iOS 10 should be served javascript_version:es5

Fixes #11234

* Update min Safari version to 12
2018-01-05 11:28:03 -08:00
Daniel Høyer Iversen 65d841c3a6 Update PULL_REQUEST_TEMPLATE.md (#11465)
* Update PULL_REQUEST_TEMPLATE.md

* Add period
2018-01-05 18:31:41 +01:00
William Scanlon 5236b720ab Catch everything when calling to OctoPrint API to fix #10557 (#11457) 2018-01-05 18:07:09 +01:00
Thibault Cohen ebbdce1f42 Update hydroquebec component to use hass httpsession (#11412)
* Update hydroquebec component to use hass httpsession

* Remove blank line
2018-01-05 10:22:40 +01:00
cdce8p 1490ccf7fb Updated gitignore file (#11452) 2018-01-05 00:03:32 -08:00
cdce8p 642e9c53ba Input Boolean - Deleted 'DEFAULT_INITIAL' (#11453) 2018-01-05 00:03:00 -08:00
cdce8p 1e9e6927eb Input Select - Added service description (#11456) 2018-01-05 00:02:10 -08:00
Paulus Schoutsen b7c7041873 Add some tests to the cloud component (#11460) 2018-01-04 21:40:18 -08:00
Thijs de Jong 35f35050ff Set tahoma cover scan interval to 60 seconds (#11447)
* Increase tahoma scan interval to 60

* import timedelta

* fix lint
2018-01-05 01:02:31 +01:00
PhracturedBlue 6cb02128b4 Reconnect to alarmdecoder on disconnect (#11383)
* Reconnect to alarmdecoder on disconnect

* Use track_point_in_time instead of call_later

* use alarmdecoder 1.13.2 which has a more robust reconnection fix
2018-01-04 20:10:56 +01:00
Marius 3436676de2 Updated generic thermostat to respect operation_mode and added away mode (#11445)
* Updated generic thermostat to respect operation_mode and added away mode

* Updated tests to include away mode and corrected init problem for sensor state
Added more tests to improve coverage and corrected again lint errors
Fixed new test by moving to correct package
Fixed bug not restoring away mode on restart

* Added support for idle on interface through state

* Added back initial_operation_mode and modified away_temp to be only one for now

* Fixed houndci-bot errors

* Added back check for None on restore temperature

* Fixed failing tests as well

* Removed unused definitions from tests

* Added use case for no initial temperature and no previously saved temperature
2018-01-04 19:05:11 +01:00
Frantz 04de22613c Added new climate component from Daikin (#10983)
* Added Daikin climate component

* Fixed tox & hound

* Place up the REQUIREMENTS var

* Update .coveragerc

* Removed unused customization

* Prevent setting invalid operation state

* Fixed hound

* Small refactor according to code review

* Fixed latest code review comments

* Used host instead of ip_address

* No real change

* No real change

* Fixed lint errors

* More pylint fixes

* Shush Hound

* Applied suggested changes for temperature & humidity settings

* Fixed hound

* Fixed upper case texts

* Fixed hound

* Fixed hound

* Fixed hound

* Removed humidity since even the device has the feature it cant be set from API

* Code review requested changes

* Fixed hound

* Fixed hound

* Trigger update after adding device

* Added Daikin sensors

* Fixed hound

* Fixed hound

* Fixed travis

* Fixed hound

* Fixed hound

* Fixed travis

* Fixed coverage decrease issue

* Do less API calls and fixed Travis failures

* Distributed code from platform to climate and sensor componenets

* Rename sensor state to device_attribute

* Fixed hound

* Updated requirements

* Simplified code

* Implemented requested changes

* Forgot one change

* Don't allow customizing temperature unit and take it from hass (FOR NOW)

* Additional code review changes applied

* Condensed import even more

* Simplify condition check

* Reordered imports

* Disabled autodiscovery FOR NOW :(

* Give more suggestive names to sensors
2018-01-04 11:05:27 +01:00
Paulus Schoutsen 2cf5acdfd2 Google Assistant -> Google Actions (#11442) 2018-01-03 17:26:58 -08:00
Andrey 4512a972d1 Climate: fix missing "|" (#11441)
* Add on/off supported feature to climate

* Lint

* Fix missing |

* lint
2018-01-04 02:26:11 +02:00
cdce8p 65df6f0013 Fix CONF_FRIENDLY_NAME (#11438) 2018-01-03 23:52:36 +01:00
Andrey eb00e54eba Add on/off supported feature to climate (#11379)
* Add on/off supported feature to climate

* Lint
2018-01-03 23:10:54 +01:00
Steve Easley ec700c2cd6 Fix errors in zigbee push state (#11386)
* Fix errors in zigbee push state

* Fix formatting

* Added comment
2018-01-03 12:48:36 -08:00
Daniel Claes 75a39352ff fix: hmip-etrv-2 now working with homeassistant (#11175)
* fix: hmip-etrv-2 now working with homeassistant (see also pull request at pyhomematic)

* fix linting issue and typo

* address comment @pvizeli

* Only use cached data in current operation mode

* fix linting issue
2018-01-03 19:36:25 +01:00
Ryan McLean 36c7fbe06a Plex api update (#11423)
* Updated plexapi to 3.0.5

* Removed removed server obj that was being used for the media URL generation as .url() is available in self._session
seasons() no longer exists -> renamed to season()
season() returns obj not list so corrected
season.index returns int, cast to str to zerofill

* Fixed Linting
2018-01-03 19:28:43 +01:00
Dave Finlay bb2191b2b0 Add support for the renaming of Yamaha Receiver Zones via configuration file. Added a test to cover the change, plus previously untested options. (#11402) 2018-01-03 19:25:16 +01:00
Paulus Schoutsen f314b6cb6c Cloud Updates (#11404)
* Verify stored keys on startup

* Handle Google Assistant messages

* Fix tests

* Don't verify expiration when getting claims

* Remove email based check

* Lint

* Lint

* Lint
2018-01-03 19:16:59 +01:00
Trevor Joynson 86e1d0f952 Account for User-Agent being non-existent, causing a TypeError (#11064)
* Account for User-Agent being non-existent, causing a TypeError

* Actually fix case of no user-agent with last resort

* Return es5 as last resort

* Update __init__.py

* Update __init__.py
2018-01-02 16:42:41 -08:00
Anders Melchiorsen 02c3ea1917 Silence redundant warnings about slow setup (#11352)
* Downgrade slow domain setup warning

* Revert "Downgrade slow domain setup warning"

This reverts commit 64472c006b.

* Remove warning for entity components

* Remove lint

* Fix original test with extra call
2018-01-02 12:16:32 -08:00
tomaszduda23 0a67195529 Fixing OpenWeatherMap Sensor. Current weather is 'unknown' if forecast: false. It was reported as #8640. (#11417) 2018-01-02 20:54:06 +01:00
Anders Melchiorsen feb70b47df Performance optimization of TP-Link switch (#11416) 2018-01-02 19:31:33 +01:00
Lukas Barth 909f613324 Do not purge the most recent state for an entity (#11039)
* Protect states that are the most recent states of their entity

* Also protect events

* Some documentation

* Fix SQL
2018-01-01 18:43:10 -08:00
Dan Nixon f0bf7b0def More support for availability reporting on MQTT components (#11336)
* Abstract MQTT availability from individual components

- Moved availability topic and payloads to MQTT base schema.
- Updated components that already report availability:
  - Switch
  - Binary sensor
  - Cover

* Add availability reporting to additional MQTT components

- Light
- JSON light
- Template light
- Lock
- Fan
- HVAC
- Sensor
- Vacuum
- Alarm control panel

* Annotate MQTT platform coroutines
2018-01-01 18:32:29 -08:00
Marius 541707c3e7 Removed status block to allow https://github.com/home-assistant/home-assistant-polymer/pull/766 with no impact (#11345) 2018-01-01 18:22:27 -08:00
Mitko Masarliev add89d6973 Notify webos timeout error fix (#11027)
* use smaller icon

* add timeout option

* remove default icon, remove timeout option

* add image again
2018-01-01 18:19:49 -08:00
NotoriousBDG e2cec9b3ae Move IMAP Email Content body to an attribute (#11096)
* Move IMAP Email Content body to an attribute

* Fix variable names
2018-01-01 18:09:40 -08:00
Paulus Schoutsen 0f914b4c20 Update frontend to 20180102.0 2018-01-01 17:20:27 -08:00
Paulus Schoutsen 1b4be0460c Log exceptions that happen during service call (#11394)
* Log exceptions that happen during service call

* Lint
2018-01-01 15:32:39 -08:00
Greg Dowling cb899a9465 Bump pywemo to fix request include problems. (#11401) 2018-01-01 15:32:26 -08:00
Otto Winter b362017206 Upgrade pychromecast to 1.0.3 (#11410) 2018-01-01 14:52:36 -08:00
Paulus Schoutsen 6e63a4ed8a Fix broken tests (#11395)
* Do not leave remember the milk config file behind

* Fix exception in service causing service timeout

* Change max service timeout to 9 to catch services timing out

* Fix Google Sync service test

* Update and pin test requirements
2018-01-01 14:30:09 -08:00
Kane610 b9c852392c Add deCONZ component (#10321)
* Base implementation of component, no sensors yet

* Added senor files

* First fully working chain of sensors and binary sensors going from hardware in to hass

* Clean up

* Clean up

* Added light platform

* Turning lights on and off and set brightness now works

* Pydeconz is now a proper pypi package
Stop sessions when Home Assistant is shutting down
Use a simpler websocket client

* Updated pydocstrings
Followed recommendations from pylint and flake8

* Clean up

* Updated requirements_all.txt

* Updated Codeowners to include deconz.py
Also re-added the Axis component since it had gotten removed

* Bump requirement

* Bumped to v2
Reran script/gen_requirements

* Removed global DECONZ since it wasn't relevant any more

* Username and password is only relevant in the context of getting a API key

* Add support for additional sensors

* Added support for groups

* Moved import of component library to inside of methods

* Moved the need for device id to library

* Bump pydeconz to v5

* Add support for colored lights

* Pylint and flake8 import improvements

* DATA_DECONZ TO DECONZ_DATA

* Add support for transition time

* Add support for flash

* Bump to v7

* ZHASwitch devices will now only generate events by default, instead of being a sensor entity

* Clean up

* Add battery sensor when device signals through an event

* Third-party library communicates with service

* Add support for effect colorloop

* Bump to pydeconz v8

* Same domain everywhere

* Clean up

* Updated requirements_all

* Generated API key will now be stored in a config file

* Change battery sensor to register to callback since library now supports multiple callbacks
Move DeconzEvent to hub
Bump to v9

* Improve entity attributes

* Change end of battery name to battery level
No need for static icon variable when using battery level helper

* Bump requirement to v10

* Improve pydocstring for DeconzEvent
Rename TYPE_AS_EVENT to CONF_TYPE_AS_EVENT

* Allow separate brightness to override RGB brightness

* Expose device.reachable in entity available property

* Bump requirement to 11 (it goes up to 11!)

* Pylint comment

* Binary sensors don't have unit of measurement

* Removed service to generate API key in favor of just generating it as a last resort of no API key is specified in configuration.yaml or deconz.conf

* Replace clear text to attribute definitions

* Use more constants

* Bump requirements to v12

* Color temp requires xy color support

* Only ZHASwitch should be an event

* Bump requirements to v13

* Added effect_list property

* Add attribute to battery sensor to easy find event id

* Bump requirements to v14

* Fix hound comment

* Bumped requirements_all information to v14

* Add service to configure devices on deCONZ

* Add initial support for scenes

* Bump requirements to v15

* Fix review comments

* Python doc string improvement

* Improve setup and error handling during setup

* Changed how to evaluate light features

* Remove 'ghost' events by not triggering updates if the signal originates from a config event
Bump requirement to v17

* Fix pylint issue by moving scene ownership in to groups in requirement pydeconz
Bump requirement to v18

* Added configurator option to register to deCONZ when unlocking gateway through settings
Bump requirement to v20

* Improve async configurator

* No user interaction for deconz.conf

* No file management in event loop

* Improve readability of load platform

* Fewer entity attributes

* Use values() instead of items() for dicts where applicable

* Do one add devices per platform

* Clean up of unused attributes

* Make sure that discovery info is not None

* Only register configure service and shutdown service when deconz has been setup properly

* Move description

* Fix lines longer than 80

* Moved deconz services to a separate file and moved hub to deconz/__init__.py

* Remove option to configure switch as entity

* Moved DeconzEvent to sensor since it is only Switch buttonpress that will be sent as event

* Added support for automatic discovery of deconz
Thanks to Kroimon for adding support to netdisco

* Use markup for configuration description

* Fix coveragerc

* Remove deCONZ support from Hue component

* Improved docstrings and readability

* Remove unnecessary extra name for storing in hass.data, using domain instead

* Improve readability by renaming all async methods
Bump to v21 - improved async naming on methods

* Fix first line not being in imperative mood

* Added logo to configurator
Let deconz.conf be visible since it will be the main config for the component after initial setup

* Removed bridge_type from new unit tests as part of removing deconz support from hue component

* Capitalize first letters of Battery Level

* Properly update state of sensor as well as reachable and battery
Bump dependency to v22

* Fix flake8 Multi-line docstring closing quotes should be on a separate line

* Fix martinhjelmares comments

Bump dependency to v23
Use only HASS aiohttp session
Change when to use 'deconz' or domain or deconz data
Clean up unused logger defines
Remove unnecessary return values
Fix faulty references to component documentation
Move callback registration to after entity has been initialized by HASS
Less inception style on pydocs ;)
Simplify loading platforms by using a for loop
Added voluptous schema for service
Yaml file is for deconz only, no need to have the domain present
Remove domain constraint when creating event title
2018-01-01 17:08:13 +01:00
Jeroen ter Heerdt 976a0fe38c Adding support for Egardia / Woonveilig version GATE-03 (#11397) 2018-01-01 02:10:52 +01:00
Paulus Schoutsen fc8b25a71f Clean up Google Assistant (#11375)
* Clean up Google Assistant

* Fix tests
2017-12-31 15:04:49 -08:00
Dan Nixon fcbf7abdaa Reverts unit conversions in TP-Link bulb (#11381)
Reverts energy and power unit conversions added in #10979 as they break
early devices.

A proper fix should be implemented in the pyhs100 library which should
return common units across all devices/firmwares.
2017-12-31 15:58:22 +01:00
ChristianKuehnel 7759ab6919 Remember the Milk - updating and completing tasks (#11069)
* Remember the Milk - updating and completing tasks

Added new feature so that tasks can be updated and completed.
For this feature a task id must be set when creating the task.

* fixed hould complaints

* fixed review comments by @MartinHjelmare

* removed unnecessary check as proposed by @MartinHjelmare
2017-12-29 19:20:36 +01:00
William Scanlon 3fd620198e Support for EcoNet water heaters (#11260)
* Support for EcoNet water heaters.

* Fixed requested changes.

* Added logging when temp or operation mode are None

* More fixes from PR review.

* Updated pyeconet version to fix natural gas water heater error. Last PR review fix.
2017-12-29 19:05:58 +01:00
Pascal Vizeli 49bc95549b Don't block on sevice call for alexa (#11358)
* Don't block on sevice call for alexa

* fix tests
2017-12-29 09:44:06 -08:00
Thibault Cohen ba0f7a4101 Fido component use now asyncio (#11244)
* Fido component use now asyncio

* Fix comments

* Fix comments 2

* Fix assertion for test error

* Update to pyfido 2.1.0
2017-12-29 18:33:11 +01:00
tschmidty69 4914ad1dd9 Ping device tracker now respects interval_seconds (#11348)
* Ping device tracker now respects interval_seconds
2017-12-29 16:18:39 +01:00
Dan Nixon f07a4684e0 Fix RGB template ordering in MQTT Light (#11362)
* Use different colour channel intensities in tests

Uses a different value for each colour channel in MQTT light tests to
properly differentiate between colour channels.

* Correct ordering of RGB channels in MQTT light
2017-12-29 15:28:20 +01:00
Paulus Schoutsen cfd78f7b02 Add HTTP endpoint for resending email confirmation (#11354) 2017-12-29 14:46:10 +01:00
Dan Nixon 2a2e6b6334 Correct units used in TP-Link energy monioring (#10979)
* Correct units used in TP-Link energy monioring

- Energy is measured in kWh for swtches
- Power is reported in mW for bulbs
- Energy is reported in Wh for bulbs

* TP-Ling energy: store units in attribute names

Stores the unit in the attrbute names for TP-Link devices that support
energy monitoring.
2017-12-29 13:13:15 +01:00
Fabian Affolter b635637541 Upgrade youtube_dl to 2017.12.28 (#11357) 2017-12-29 10:16:18 +01:00
Fabian Affolter a7ebba6863 Upgrade python-telegram-bot to 9.0.0 (#11341) 2017-12-29 10:08:14 +01:00
Fabian Affolter 1f8acb49bc Upgrade mypy to 0.560 (#11334) 2017-12-29 10:07:25 +01:00
Fabian Affolter 5a4bca9780 Upgrade sqlalchemy to 1.2.0 (#11333) 2017-12-29 10:07:04 +01:00
Fabian Affolter d7e52d8014 Upgrade pyowm to 2.8.0 (#11332) 2017-12-29 10:06:52 +01:00
Fabian Affolter 391a8901c8 Upgrade fuzzywuzzy to 0.16.0 (#11331) 2017-12-29 10:06:39 +01:00
Fabian Affolter b98e03b5bc Upgrade aiohttp to 2.3.7 (#11329) 2017-12-29 10:06:25 +01:00
Andy Castille 6b586c268a DoorBird feature update (#11193)
* Allow disabling the DoorBird camera live view

* Support for push notifications from DoorBird devices

* use DoorBirdPy 0.1.1 instead of 0.1.0

* Fix lint errors in DoorBird binary sensor

* Change DoorBird push notifications from binary sensor to event

* Remove DoorBird camera options

* use DoorBirdPy 0.1.2 to fix history image urls

* clean up doorbird event code and remove unused doorbird camera imports

* use async for doorbird doorbell events

* Minor changes

* Update file header

* Fix my mess

* Fix docstring
2017-12-29 10:05:45 +01:00
goldminenine 9a34e7174c Update modbus.py (#11238)
Support of MODBUS RTU over TCP ethernet mode. See more description here:  https://www.eltima.com/modbus-over-ethernet/
2017-12-29 09:19:34 +01:00
Fabian Affolter 3203849b60 Move data instance to setup (#11350) 2017-12-29 09:03:03 +01:00
Thom Troy 2e582a4597 pass stops_at to get_station_by_name (#11304) 2017-12-28 22:37:51 +01:00
Gregory Benner 5d38eec37d Sochain cryptocurrency sensor (#11335)
* add required files

* add sochain sensor

* add missing schema

* end first sentence with a period to make travis happy

* upgrade to python-sochain-api 0.0.2 and use asyncio

* add missing _LOGGER and fix long line

* move object setup to async_setup_platform

* rename chainSo variable to chainso
2017-12-28 21:39:24 +01:00
Dan Nixon a6c7fe04da Add default names and validation for TP-Link devices (#11346)
Adds missing platform schema for TP-Link smart sockets and adds default
names for smart sockets and bulbs.
2017-12-28 21:22:46 +01:00
Jeroen ter Heerdt 966ab20f26 Remove need for separate EgardiaServer setup (#11344)
* Removing need for separate Egardiaserver setup

* Fixing linting errors

* Updating egardia component based on review

* Updating egardia component based on review

* Updating egardia component based on review

* Removed return False twice based on review
2017-12-28 20:20:44 +01:00
Matt Schmitt b2e9dc5c8f Additional device classes for binary sensors (#11280)
* Add additional device classes for binary sensor
2017-12-28 18:55:22 +01:00
Lukas Barth fc3a8e4e79 Merge pull request #10535 from broox/nuheat
Adding a Nuheat Thermostat component
2017-12-28 09:14:27 +01:00
Derek Brooks 00352d41a7 remove return value as requested 2017-12-27 18:20:12 -08:00
Derek Brooks 29c26e0015 fix bad nuheat component test 2017-12-27 13:06:04 -08:00
Derek Brooks 63d9bd4a9c test resume program service 2017-12-27 12:42:56 -08:00
Bob Igo cb23549af6 closes #11314 by not restricting the voice to anything but a string (#11326) 2017-12-27 19:49:06 +01:00
Rene Nulsch e5cc5a58e1 Bugfix for HA Issue 7292, 9412 - switch to gamertag to receive ssl image url (#11315) 2017-12-27 09:24:37 +01:00
Greg Laabs aa8db784d5 Fix leak sensors always showing Unknown until Wet (#11313)
Leak sensors were using the "wet" node as a negative node, which prevented them from ever gettng a Dry status unless the user pressed the button on the hardware after every Hass reboot.

This change ignores the Wet node, as it is just always the exact inverse of the Dry node. We don't need to watch both.
2017-12-27 09:23:21 +01:00
Fabian Affolter d68d4d1129 Upgrade alpha_vantage to 1.6.0 (#11307) 2017-12-27 09:21:28 +01:00
Fabian Affolter 8d32e883bd Upgrade youtube_dl to 2017.12.23 (#11308) 2017-12-27 09:21:07 +01:00
Fabian Affolter 7826b9aa72 Upgrade python-digitalocean to 1.13.2 (#11311) 2017-12-27 09:20:44 +01:00
Fabian Affolter e91d47db96 Upgrade distro to 1.2.0 (#11312) 2017-12-27 09:20:08 +01:00
Fabian Affolter e92e433805 Upgrade yahooweather to 0.10 (#11309) 2017-12-27 09:19:46 +01:00
Fabian Affolter 40e1d35268 Upgrade luftdaten to 0.1.3 (#11316) 2017-12-27 09:19:02 +01:00
Fabian Affolter af6c39f4d1 Upgrade pysnmp to 4.4.3 (#11317) 2017-12-27 09:17:43 +01:00
Fabian Affolter 5be949f00f Upgrade aiohttp_cors to 0.6.0 (#11310) 2017-12-27 09:17:03 +01:00
Mitko Masarliev 169459b57f Fix for track_new_devices BC (#11202)
* BC fix

* more tests

* inline if change

* inline if change
2017-12-26 14:49:24 -08:00
Conrad Juhl Andersen 05926b1994 xiaomi_aqara: Fix covers never being closed (#11319)
Bug in equality testing
2017-12-26 23:35:48 +01:00
awkwardDuck 54b4142530 Fix typo in bitcoin.py component for mined blocks. (#11318) 2017-12-26 22:02:59 +01:00
Derek Brooks f0244d7982 add a bit more test coverage 2017-12-26 11:12:28 -08:00
Paulus Schoutsen 7a600ea064 Add heartbeat to websocket connections (#11298) 2017-12-26 10:59:41 +01:00
Greg Laabs d687bc073e Huge ISY994 platform cleanup, fixes support for 5.0.10 firmware (#11243)
* Huge ISY994 platform cleanup, fixes support for 5.0.10 firmware

# * No more globals - store on hass.data
# * Parent ISY994 component handles categorizing nodes in to Hass components, rather than each individual domain filtering all nodes themselves
# * Remove hidden string, replace with ignore string. Hidden should be done via the customize block; ignore fully prevents the node from getting a Hass entity
# * Removed a few unused methods in the ISYDevice class
# * Cleaned up the hostname parsing
# * Removed broken logic in the fan Program component. It was setting properties that have no setters
# * Added the missing SUPPORTED_FEATURES to the fan component to indicate that it can set speed
# * Added better error handling and a log warning when an ISY994 program entity fails to initialize
# * Cleaned up a few instances of unecessarily complicated logic paths, and other cases of unnecessary logic that is already handled by base classes

* Use `super()` instead of explicit base class calls

* Move `hass` argument to first position

* Use str.format instead of string addition

* Move program structure building and validation to component

Removes the need for a bunch of duplicate exception handling in each individual platform

* Fix climate nodes, fix climate names, add config to disable climate

Sensor platform was crashing when the ISY reported climate nodes. Logic has been fixed. Also added a config option to prevent climate sensors from getting imported from the ISY. Also replace the underscore from climate node names with spaces so they default to friendly names.

* Space missing in error message

* Fix string comparison to use `==`

* Explicitly check for attributes rather than catch AttributeError

Also removes two stray debug lines

* Remove null checks on hass.data, as they are always null at this point
2017-12-26 09:26:37 +01:00
Mike Megally a59b02b6b4 Removed error log used as debug (#11301)
An error was being logged to display debug info. Removed it
2017-12-25 20:49:02 +01:00
Frantz 14919082a3 Better error handling (#11297)
* Better error handling

* Fixed hound
2017-12-25 17:46:42 +01:00
Bob Anderson 802a95eac5 Fix unpredictable entity names in concord232 binary_sensor (#11292) 2017-12-25 13:26:22 +01:00
Michael Irigoyen a44181fd35 Add Chime status and control to Alarm Decoder component (#11271)
* Enable more alarm decoder attributes, including chime status and ready status

* Expose chime service in the alarm decoder component

* Fix line length linting issue

* Fix spacing lint issue

* Update PR based on reviewer requests

* Update based on linting catches

* Fix descriptions include from async to sync
2017-12-25 11:34:07 +01:00
PhracturedBlue b280a791a6 Store raw state of RF sensors from alarmdecoder (#10841)
* Store raw state of RF sensors from alarmdecoder

* Fix resync.  Fix issue with RFID not being truly optional

* Breakdown RF attributes per bit

* Preserve import style
2017-12-25 10:52:33 +01:00
Jordy 7269070d97 Added rainsensor (#11023)
* Added rainsensor

Added rainsensor

* Added to coverage ignore

* Fixed issues

* script\gen_requirements_all.py

script\gen_requirements_all.py

* Gen requirements

* requirements

* requirements

* Fix docstring

* Fix log message

* Revert change
2017-12-25 10:07:17 +01:00
Phil Kates 94ac0b5ed8 alexa: Add handling for covers (#11242)
* alexa: Add handling for covers

Covers don't support either cover.turn_on or homeassistant.turn_on so
use cover.[open|close]_cover.

* alexa: Add tests for covers
2017-12-24 15:05:56 -08:00
Derek Brooks 8ef8dbc868 pleasin the hound 2017-12-24 11:15:50 -07:00
Derek Brooks 7de3c62846 register nuheat_resume_program service 2017-12-24 11:10:22 -07:00
Derek Brooks fb90dab471 add ability to change the Nuheat thermostat hold mode 2017-12-24 10:09:27 -07:00
Derek Brooks 419ec7f7a7 bump to python-nuheat 0.3.0 2017-12-24 09:43:56 -07:00
Derek Brooks bdf64ccbbb Merge branch 'dev' into nuheat 2017-12-24 09:40:21 -07:00
Ryan McLean 3fa45375d9 Plex refactor (#11235)
* Cleaned up '_clear_media()'

* Moved media Type to new method

* renamed "clear_media()' to ' clear_media_details()'
reset 'app_name' (Library Name) in clear_media_details
moved thumbs to '_set_media_image()'

* Moved playback info into setmedia type as it was just used for the next anyway

* Moved library name & image download to only happen if session and player active as else no point anyway

* Fixed Linting issue

* Some tweaks to clean up unintended complexity

* Removed redundant declarations

* Fixed whitespace

* Revert "Fixed whitespace"

This reverts commit 0985445c47.

* Revert "Removed redundant declarations"

This reverts commit 6f9d5a85b0.
2017-12-24 11:18:31 -05:00
Aaron Bach 5566ea8c81 Adds support for disabled Tiles and automatic session renewal (#11172)
* Adds support for disabled Tiles and automatic session renewal

* Updated requirements

* Collaborator-requested changes

* Collaborator-requested changes
2017-12-24 01:19:04 +01:00
Andrea Campi 8c303bf48c Support multiple Hue bridges with lights of the same id (#11259)
* Improve support for multiple Hue bridges with lights that have the same id.

The old code pre-refactoring kept a per-bridge list of lights in a closure; my refactoring moved that to hass.data, which is convenient but caused them to conflict with each other.

Fixes #11183

* Update test_hue.py
2017-12-24 01:12:54 +01:00
David Fiel 8683d75aa1 Greenwave Reality (TCP Connected) Lighting Component (#11282)
* Create greenwave.py

* Update .coveragerc

* Update requirements_all.txt

* Update greenwave.py

Line too long

* Update greenwave.py

* Update requirements_all.txt

* Update greenwave.py

* Update greenwave.py

* fix style
2017-12-24 01:11:45 +01:00
Andrey Kupreychik 4f5d7cea11 Added password for GPS logger endpoint (#11245)
* Added password for GPS logger endpoint

* Fixed lint error

* Update gpslogger.py

* fix lint

* fix import
2017-12-24 00:15:06 +01:00
Andrey ab9ffc4f05 Report Sensibo as off when it is off (#11281) 2017-12-23 11:10:54 +01:00
Paulus Schoutsen 6e2bfcfe65 Update frontend to 20171223.0 2017-12-22 21:31:31 -08:00
Andrey 240098dd7e Change manifest path to /states as this is the path / actually sets. (#11274) 2017-12-22 21:05:15 -08:00
Daniel Watkins 353bb62687 Fix webostv select source (#11227)
* Fix reuse of variable name

This should fix #11224.

* Add tests for LgWebOSDevice.select_source
2017-12-22 18:38:00 +01:00
maxlaverse 46df91ff45 Fix allday events in custom_calendars (#11272) 2017-12-22 14:08:34 +01:00
Ben Randall 295caeb065 Fix async IO in Sesame lock component. (#11054)
* Call update on Sesame devices to cache initial state

* Switch to using async_add_devices

* Fix line length

* Fix Lint errors

* Fix more Lint errors

* Cache pysesame properties

* Updates from CR feedback
2017-12-22 10:28:51 +01:00
Egor Tsinko eeb309aea1 Functinality to save/restore snapshots for monoprice platform (#10296)
* added functionality to save/restore snapshots to monoprice platform

* renamed monoprice_snapshot, monoprice_restore to snapshot, restore

This is to simplify refactoring of snapshot/restore functionality for monoprice, snapcast and sonos in the future
2017-12-22 10:26:34 +01:00
Pascal Vizeli 9e0a765801 Revert "Backup configuration files before overwriting" (#11269)
* Revert "Adding MotionIP to BinarySensors for HMIP-SMI (#11268)"

This reverts commit c94cc34a8f.

* Revert "Bugfix: 10509 - http is hard coded in plex sensor (#11072)"

This reverts commit 901d4b5489.

* Revert "Fix handling zero values for state_on/state_off (#11264)"

This reverts commit 2e4e3a42cc.

* Revert "Fix inverted sensors on the concord232 binary sensor component (#11261)"

This reverts commit b866687cd7.

* Revert "Proper Steam game names and small fixes (#11182)"

This reverts commit 7faa94046c.

* Revert "Bugfix homematic available modus (#11256)"

This reverts commit 1d579587c1.

* Revert "Fix detection of if a negative node is in use (#11255)"

This reverts commit b28bfad496.

* Revert "added myself to become code owner for miflora and plant (#11251)"

This reverts commit e0682044f0.

* Revert "Add workaround for running tox on Windows platforms (#11188)"

This reverts commit 81f1a65fae.

* Revert "Upgrade to new miflora version 0.2.0 (#11250)"

This reverts commit 8efc4b5ba9.

* Revert "homematic: add username and password to interface config schema (#11214)"

This reverts commit b4e2537de3.

* Revert "Backup configuration files before overwriting (#11216)"

This reverts commit 90e25a6dfb.
2017-12-21 22:33:37 +01:00
schnoetz c94cc34a8f Adding MotionIP to BinarySensors for HMIP-SMI (#11268)
* Adding MotionIP to BinarySensors for HMIP-SMI

My HmIP-SMI (Homematic IP Motion Sensor) only shows "ILLUMINATION" and no MOTION, because the binary values are not recognized. The "old" homematic-motion detectors are working well showing motion, too. 
I found out that "MotionIP" was missing at the binary_sensors - after adding "Motion" and "Motion Detection Activated" are shown.

* Removed trailing blanks

removed trailing blanks from my previous change
2017-12-21 21:46:42 +01:00
Ryan McLean 901d4b5489 Bugfix: 10509 - http is hard coded in plex sensor (#11072)
* Fix for sensor no ssl

* Line length Fixes

* Removed unneeded schema extensions

* Removed unrequired variable

* Added defaults for SSL & SSL Verify

* Moved Defaults to Variables

Corrected SSL Defaults to match the other Defaults style

* Fixed Typo

* Removed option to disable verify ssl

* Removed unused import

* Removed unused CONST

* Fixed error handling

* Cleanup of unneeded vars & logging

* Fix for linting
2017-12-21 10:24:57 -05:00
Zio Tibia 2e4e3a42cc Fix handling zero values for state_on/state_off (#11264) 2017-12-21 14:24:19 +01:00
CTLS b866687cd7 Fix inverted sensors on the concord232 binary sensor component (#11261)
* Fix inverted sensors on the concord232 binary sensor component

* Changed from == Tripped to != Normal
2017-12-21 06:29:42 +01:00
Frank Wickström 7faa94046c Proper Steam game names and small fixes (#11182)
* Use constant for offline state

* Use constant for no game name

* Rename trade and play constant their proper names

Trade and Play are not the correct names for the states. For instance
Play might be seens as the user is actually is playing, which is not
correct as there is no such state is returned from the Steam API.
Just having "trade" does not say much about what is happening and
might be misintepreted that the user is currently trading, which is not
correct either.

We instead use the names from the underlying library for naming the
states [1]

[1] https://github.com/Lagg/steamodd/blob/2e518ad84f3afce631d5d7eca3af0f85b5330b5b/steam/user.py#L109

* Get the proper game name if no extra info is given from the api

The third `current_game` parameter that was used before hold
extra information about the game that is being played. This might
contain the game name, it might also be empty. The correct way to
get the game name is to fetch it from the API depending on the
game id that might be returned in the `current_game` attribute if
the user is playing a game.

To not break existing implementations we keep the functionality
to first go with the extra information and only then fetch the proper
game name.

* Refactor getting game name to its own function

This cleans up the code and removed "ugly" else statements
from the sensor and makes the game fetching easier to read.

* Let state constant values be lower snake case

* Return None instead of 'None' when no current game exists

* Initialize steam app list only once to benefit form caching

* Return None as state attributes if no current game is present
2017-12-20 22:32:33 -05:00
Pascal Vizeli 1d579587c1 Bugfix homematic available modus (#11256) 2017-12-20 23:59:11 +01:00
Greg Laabs b28bfad496 Fix detection of if a negative node is in use (#11255)
* Fix detection of if a negative node is in use

Fix a problem where every negative node gets detected as in-use. Code was not checking the correct property.

* Allow protected access
2017-12-20 23:58:22 +01:00
ChristianKuehnel e0682044f0 added myself to become code owner for miflora and plant (#11251) 2017-12-20 12:11:56 +01:00
Ben Randall 81f1a65fae Add workaround for running tox on Windows platforms (#11188)
* Add workaround for running tox on Windows platforms

* Remove install_command override
2017-12-20 11:50:31 +01:00
ChristianKuehnel 8efc4b5ba9 Upgrade to new miflora version 0.2.0 (#11250) 2017-12-20 11:35:03 +01:00
Janne Grunau b4e2537de3 homematic: add username and password to interface config schema (#11214)
Fixes #11191, the json-rpc name resolving method requires user account
and password.
2017-12-20 00:38:59 +01:00
Charles Garwood 90e25a6dfb Backup configuration files before overwriting (#11216)
* Backup configuration files before overwriting

* Changed timestamp format from epoch to iso8601 (minus punctuation)
2017-12-19 17:55:24 +01:00
Dan Chen 3d90855ca6 Bump python-miio version (#11232) 2017-12-19 08:22:13 +01:00
Dan Nixon 200c927087 Extend Threshold binary sensor to support ranges (#11110)
* Extend Threshold binary sensor to support ranges

- Adds support for ranges
- Threshold type (lower, upper, range) is defined by supplied
  thresholds (lower, upper)
- Adds verbose status/position relative to threshold as attribute
  (position)

* Minor changes (ordering, names, etc.)

* Update name

* Update name
2017-12-19 01:52:19 +01:00
markferry ef22a6e18d Fix statistics sensor mean and median when only one sample is available. (#11180)
* Fix statistics sensor mean and median when only one sample is available.

With only one data point stddev and variance throw an exception.
This would clear the (valid) mean and median calculations.

Separate the try..catch blocks for one-or-more and two-or-more stats so
that this doesn't happen.

Test this with a new sampling_size_1 test.

* test_statistics trivial whitespace fix
2017-12-18 21:21:27 +01:00
Thibault Maekelbergh 061395d2f8 Add Discogs Sensor platform (#10957)
* Add Discogs Sensor platform

* Add discogs module to requirements_all

* Fix wrong style var name

* PR Feedback (scan interval, mod. docstring)

* Added sensor.discogs to coveragerc

* Use SERVER_SOFTWARE helper for UA-String

* Don't setup platform if token is invalid

* Fix trailing whitespace for Hound CI

* Move client setup to setup()
2017-12-18 19:10:54 +01:00
Khole 05258ea4bf Hive Component Release Two (#11053)
* Add boost functionality to climate devices

* Update boost target temperature rounding

* Update with Colour Bulb Support

* colour bulb fix

* Requirements Update and colorsys import

* Add RGB Attribute - ATTR_RGB_COLOR

* Hive release-2

* add boost support for hive climate platform

* Colour Bulb - Varible update

* Boost - Tox error

* Convert colour to color

* Correct over indentation

* update version to 0.2.9 pyhiveapi

* Updated pyhiveapi to version 2.10 and altertered turn_n on fuction to 1 call

* Update climate doc string

* Update to is_aux_heat_on

* update to is_aux_heat_on
2017-12-18 18:15:41 +01:00
Thibault Cohen 8742ce035a Hydroquebec component use now asyncio (#10795)
* Hydroquebec component use now asyncio

* Add tests

* Improve coverage

* fix tests

* Remove useless try/except and associated tests
2017-12-17 22:11:48 +01:00
maxlaverse 0664bf31a2 Fix webdav calendar schema (#11185) 2017-12-17 20:53:40 +01:00
Fabian Affolter 0ec1ff642d Bump dev to 0.61.0.dev0 2017-12-17 16:29:36 +01:00
Fabian Affolter cd44824233 Merge branch 'master' into dev 2017-12-17 16:24:52 +01:00
Brad Dixon dfb8b5a3c1 Revbump to SoCo 0.13 and add support for Night Sound and Speech Enhancement. (#10765)
Sonos Playbar and Playbase devices support Night Sound and Speech Enhancement
effects when playing from sources such as a TV. Adds a new service "sonos_set_option"
whichs accepts boolean options to control these audio features.
2017-12-17 13:08:35 +01:00
Przemek Więch 024f1d4882 Try multiple methods of getting data in asuswrt. (#11140)
* Try multiple methods of getting data in asuswrt.

Solves #11108 and potentially #8112.

* fix style

* fix lint
2017-12-17 12:46:47 +01:00
PhracturedBlue 3375261f51 convert alarmdecoder interface from async to sync (#11168)
* convert alarmdecoder interface from async to sync

* Convert he rest of alarmdecoder rom async to sync

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py
2017-12-17 00:52:59 +01:00
Matthew Treinish 640d58f0a8 Fix X10 commands for mochad light turn on (#11146)
* Fix X10 commands for mochad light turn on

This commit attempts to address issues that a lot of people are having
with the x10 light component. Originally this was written to use the
xdim (extended dim) X10 command. However, not every X10 dimmer device
supports the xdim command. Additionally, it turns out the number of
dim/brighness levels the X10 device supports is device specific and
there is no way to detect this (given the mostly 1 way nature of X10)

To address these issues, this commit removes the usage of xdim and
instead relies on using the 'on' command and the 'dim' command. This
should work on all x10 light devices. In an attempt to address the
different dim/brightness levels supported by different devices this
commit also adds a new optional config value, 'brightness_levels', to
specify if it's either 32, 64, or 256. By default 32 levels are used
as this is the normal case and what is documented by mochad.

Fixes #8943

* make code more readable

* fix style

* fix lint

* fix tests
2017-12-17 00:52:40 +01:00
Mike Megally 793b8b8ad3 Remove logging (#11173)
An error was being log that seems more like debug info
2017-12-16 22:29:40 +01:00
Pascal Vizeli 39af43eb5c Add install mode to homematic (#11164) 2017-12-16 14:22:23 +01:00
Paulus Schoutsen b56675a7bb Don't connect to cloud if subscription expired (#11163)
* Final touch for cloud component

* Fix test
2017-12-16 00:42:25 -08:00
Adam Mills c4d71e934d Perform logbook filtering on the worker thread (#11161) 2017-12-16 00:04:27 -08:00
Paulus Schoutsen 3d5d90241f Update frontend 2017-12-15 23:35:37 -08:00
Pascal Vizeli a7c8e202aa Resolve hostnames (#11160) 2017-12-15 22:54:54 +01:00
Pascal Vizeli a63658d583 Homematic next (#11156)
* Cleanup logic & New gen of HomeMatic

* fix lint

* cleanup

* fix coverage

* cleanup

* name consistenc

* fix lint

* Rename ip

* cleanup wrong property

* fix bug

* handle callback better

* fix lint

* Running now
2017-12-15 21:22:36 +01:00
Derek Brooks 3193e825d5 remove nuheat away functionality. :( 2017-12-06 22:34:13 -06:00
Derek Brooks c262a387dc add supported_features functionality 2017-12-06 22:24:54 -06:00
Derek Brooks 36d5fff8e0 address feedback on log lines 2017-12-06 21:52:44 -06:00
Derek Brooks 72aa722c33 Merge branch 'dev' into nuheat 2017-12-06 21:49:13 -06:00
Derek Brooks a9feafd571 add nuheat to coverage reports 2017-11-18 10:26:36 -06:00
Derek Brooks a3c6211c04 python 3.5 seems to not like assert_called_once() 2017-11-13 23:20:16 -06:00
Derek Brooks afcb0b8767 fix up some docstrings 2017-11-13 23:13:04 -06:00
Derek Brooks c0c439c549 that int() casting was redundant 2017-11-13 22:45:29 -06:00
Derek Brooks 959f6386b4 shorten that long line 2017-11-13 22:43:11 -06:00
Derek Brooks f1fe8e95ba clean up a couple away temperature settings 2017-11-13 22:40:18 -06:00
Derek Brooks 6892033556 remove that unused constant 2017-11-13 12:31:15 -06:00
Derek Brooks 766893253a make sure is_away_mode_on supports user-defined minimum away temps 2017-11-13 12:28:09 -06:00
Derek Brooks ef5edb95ba Update home/auto hold mode to be consistent with current documentation 2017-11-13 11:38:08 -06:00
Derek Brooks f21b9988e9 allow for the configuring of a minimum away temperature 2017-11-13 11:00:33 -06:00
Derek Brooks 7859b76429 kill target_temperature_low and high. They don't make sense here 2017-11-13 10:35:48 -06:00
Derek Brooks 2c44e4fb12 address initial houndbot suggestions 2017-11-11 16:47:12 -06:00
Derek Brooks 9b373901fa add documentation links 2017-11-11 16:31:35 -06:00
Derek Brooks 37be81c20c add ability to resume program... and add in a forgotten test 2017-11-11 16:21:14 -06:00
Derek Brooks 5fe2db228c bug fixes and linting 2017-11-11 10:18:32 -06:00
Derek Brooks c91d52a587 first stab at the nuheat components 2017-11-11 00:22:37 -06:00
370 changed files with 11224 additions and 4045 deletions
+18 -3
View File
@@ -50,9 +50,15 @@ omit =
homeassistant/components/bloomsky.py
homeassistant/components/*/bloomsky.py
homeassistant/components/coinbase.py
homeassistant/components/sensor/coinbase.py
homeassistant/components/comfoconnect.py
homeassistant/components/*/comfoconnect.py
homeassistant/components/deconz/*
homeassistant/components/*/deconz.py
homeassistant/components/digital_ocean.py
homeassistant/components/*/digital_ocean.py
@@ -263,6 +269,9 @@ omit =
homeassistant/components/zoneminder.py
homeassistant/components/*/zoneminder.py
homeassistant/components/daikin.py
homeassistant/components/*/daikin.py
homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/canary.py
homeassistant/components/alarm_control_panel/concord232.py
@@ -296,6 +305,7 @@ omit =
homeassistant/components/camera/rpi_camera.py
homeassistant/components/camera/synology.py
homeassistant/components/camera/yi.py
homeassistant/components/climate/econet.py
homeassistant/components/climate/ephember.py
homeassistant/components/climate/eq3btsmart.py
homeassistant/components/climate/flexit.py
@@ -307,6 +317,7 @@ omit =
homeassistant/components/climate/proliphix.py
homeassistant/components/climate/radiotherm.py
homeassistant/components/climate/sensibo.py
homeassistant/components/climate/touchline.py
homeassistant/components/cover/garadget.py
homeassistant/components/cover/homematic.py
homeassistant/components/cover/knx.py
@@ -365,8 +376,10 @@ omit =
homeassistant/components/light/decora.py
homeassistant/components/light/decora_wifi.py
homeassistant/components/light/flux_led.py
homeassistant/components/light/greenwave.py
homeassistant/components/light/hue.py
homeassistant/components/light/hyperion.py
homeassistant/components/light/iglo.py
homeassistant/components/light/lifx.py
homeassistant/components/light/lifx_legacy.py
homeassistant/components/light/limitlessled.py
@@ -476,6 +489,7 @@ omit =
homeassistant/components/notify/yessssms.py
homeassistant/components/nuimo_controller.py
homeassistant/components/prometheus.py
homeassistant/components/rainbird.py
homeassistant/components/remember_the_milk/__init__.py
homeassistant/components/remote/harmony.py
homeassistant/components/remote/itach.py
@@ -504,6 +518,7 @@ omit =
homeassistant/components/sensor/deluge.py
homeassistant/components/sensor/deutsche_bahn.py
homeassistant/components/sensor/dht.py
homeassistant/components/sensor/discogs.py
homeassistant/components/sensor/dnsip.py
homeassistant/components/sensor/dovado.py
homeassistant/components/sensor/dte_energy_bridge.py
@@ -517,7 +532,6 @@ omit =
homeassistant/components/sensor/etherscan.py
homeassistant/components/sensor/fastdotcom.py
homeassistant/components/sensor/fedex.py
homeassistant/components/sensor/fido.py
homeassistant/components/sensor/fitbit.py
homeassistant/components/sensor/fixer.py
homeassistant/components/sensor/fritzbox_callmonitor.py
@@ -532,7 +546,6 @@ omit =
homeassistant/components/sensor/haveibeenpwned.py
homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/htu21d.py
homeassistant/components/sensor/hydroquebec.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/imap_email_content.py
homeassistant/components/sensor/influxdb.py
@@ -570,6 +583,7 @@ omit =
homeassistant/components/sensor/pyload.py
homeassistant/components/sensor/qnap.py
homeassistant/components/sensor/radarr.py
homeassistant/components/sensor/rainbird.py
homeassistant/components/sensor/ripple.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/scrape.py
@@ -580,6 +594,7 @@ omit =
homeassistant/components/sensor/skybeacon.py
homeassistant/components/sensor/sma.py
homeassistant/components/sensor/snmp.py
homeassistant/components/sensor/sochain.py
homeassistant/components/sensor/sonarr.py
homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/steam_online.py
@@ -648,9 +663,9 @@ omit =
homeassistant/components/vacuum/xiaomi_miio.py
homeassistant/components/weather/bom.py
homeassistant/components/weather/buienradar.py
homeassistant/components/weather/darksky.py
homeassistant/components/weather/metoffice.py
homeassistant/components/weather/openweathermap.py
homeassistant/components/weather/yweather.py
homeassistant/components/weather/zamg.py
homeassistant/components/zeroconf.py
homeassistant/components/zwave/util.py
+1
View File
@@ -11,6 +11,7 @@
```
## Checklist:
- [ ] The code change is tested and works locally.
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)
+1
View File
@@ -74,6 +74,7 @@ pip-selfcheck.json
venv
.venv
Pipfile*
share/*
# vimmy stuff
*.swp
+4 -1
View File
@@ -53,10 +53,11 @@ homeassistant/components/light/yeelight.py @rytilahti
homeassistant/components/media_player/kodi.py @armills
homeassistant/components/media_player/monoprice.py @etsinko
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
homeassistant/components/plant.py @ChristianKuehnel
homeassistant/components/sensor/airvisual.py @bachya
homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/miflora.py @danielhiversen
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/sytadin.py @gautric
homeassistant/components/sensor/tibber.py @danielhiversen
homeassistant/components/sensor/waqi.py @andrey-git
@@ -64,9 +65,11 @@ homeassistant/components/switch/rainmachine.py @bachya
homeassistant/components/switch/tplink.py @rytilahti
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/axis.py @kane610
homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/hive.py @Rendili @KJonline
homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/*/deconz.py @kane610
homeassistant/components/*/rfxtrx.py @danielhiversen
homeassistant/components/velux.py @Julius2342
homeassistant/components/*/velux.py @Julius2342
+7 -20
View File
@@ -10,7 +10,6 @@ Component design guidelines:
import asyncio
import itertools as it
import logging
import os
import homeassistant.core as ha
import homeassistant.config as conf_util
@@ -111,11 +110,6 @@ def async_reload_core_config(hass):
@asyncio.coroutine
def async_setup(hass, config):
"""Set up general services related to Home Assistant."""
descriptions = yield from hass.async_add_job(
conf_util.load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml')
)
@asyncio.coroutine
def async_handle_turn_service(service):
"""Handle calls to homeassistant.turn_on/off."""
@@ -155,14 +149,11 @@ def async_setup(hass, config):
yield from asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register(
ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service,
descriptions[ha.DOMAIN][SERVICE_TURN_OFF])
ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service)
hass.services.async_register(
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service,
descriptions[ha.DOMAIN][SERVICE_TURN_ON])
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service)
hass.services.async_register(
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service,
descriptions[ha.DOMAIN][SERVICE_TOGGLE])
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
@asyncio.coroutine
def async_handle_core_service(call):
@@ -187,14 +178,11 @@ def async_setup(hass, config):
hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE))
hass.services.async_register(
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service,
descriptions[ha.DOMAIN][SERVICE_HOMEASSISTANT_STOP])
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
hass.services.async_register(
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service,
descriptions[ha.DOMAIN][SERVICE_HOMEASSISTANT_RESTART])
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service)
hass.services.async_register(
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service,
descriptions[ha.DOMAIN][SERVICE_CHECK_CONFIG])
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service)
@asyncio.coroutine
def async_handle_reload_config(call):
@@ -209,7 +197,6 @@ def async_setup(hass, config):
hass, conf.get(ha.DOMAIN) or {})
hass.services.async_register(
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config,
descriptions[ha.DOMAIN][SERVICE_RELOAD_CORE_CONFIG])
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config)
return True
-8
View File
@@ -7,11 +7,9 @@ https://home-assistant.io/components/abode/
import asyncio
import logging
from functools import partial
from os import path
import voluptuous as vol
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME, ATTR_ENTITY_ID, CONF_USERNAME,
CONF_PASSWORD, CONF_EXCLUDE, CONF_NAME, CONF_LIGHTS,
@@ -188,22 +186,16 @@ def setup_hass_services(hass):
for device in target_devices:
device.trigger()
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))[DOMAIN]
hass.services.register(
DOMAIN, SERVICE_SETTINGS, change_setting,
descriptions.get(SERVICE_SETTINGS),
schema=CHANGE_SETTING_SCHEMA)
hass.services.register(
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image,
descriptions.get(SERVICE_CAPTURE_IMAGE),
schema=CAPTURE_IMAGE_SCHEMA)
hass.services.register(
DOMAIN, SERVICE_TRIGGER, trigger_quick_action,
descriptions.get(SERVICE_TRIGGER),
schema=TRIGGER_SCHEMA)
-7
View File
@@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation.
https://home-assistant.io/components/ads/
"""
import os
import threading
import struct
import logging
@@ -14,7 +13,6 @@ from collections import namedtuple
import voluptuous as vol
from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \
EVENT_HOMEASSISTANT_STOP
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyads==2.2.6']
@@ -107,13 +105,8 @@ def setup(hass, config):
except pyads.ADSError as err:
_LOGGER.error(err)
# load descriptions from services.yaml
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(
DOMAIN, SERVICE_WRITE_DATA_BY_NAME, handle_write_data_by_name,
descriptions[SERVICE_WRITE_DATA_BY_NAME],
schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME
)
@@ -7,7 +7,6 @@ https://home-assistant.io/components/alarm_control_panel/
import asyncio
from datetime import timedelta
import logging
import os
import voluptuous as vol
@@ -15,7 +14,6 @@ from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS)
from homeassistant.config import load_yaml_config_file
from homeassistant.loader import bind_hass
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
@@ -148,14 +146,10 @@ def async_setup(hass, config):
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
for service in SERVICE_TO_METHOD:
hass.services.async_register(
DOMAIN, service, async_alarm_service_handler,
descriptions.get(service), schema=ALARM_SERVICE_SCHEMA)
schema=ALARM_SERVICE_SCHEMA)
return True
@@ -7,23 +7,39 @@ https://home-assistant.io/components/alarm_control_panel.alarmdecoder/
import asyncio
import logging
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
import homeassistant.helpers.config_validation as cv
from homeassistant.components.alarmdecoder import (
DATA_AD, SIGNAL_PANEL_MESSAGE)
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED)
ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['alarmdecoder']
SERVICE_ALARM_TOGGLE_CHIME = 'alarmdecoder_alarm_toggle_chime'
ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({
vol.Required(ATTR_CODE): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up for AlarmDecoder alarm panels."""
add_devices([AlarmDecoderAlarmPanel()])
device = AlarmDecoderAlarmPanel()
add_devices([device])
return True
def alarm_toggle_chime_handler(service):
"""Register toggle chime handler."""
code = service.data.get(ATTR_CODE)
device.alarm_toggle_chime(code)
hass.services.register(
alarm.DOMAIN, SERVICE_ALARM_TOGGLE_CHIME, alarm_toggle_chime_handler,
schema=ALARM_TOGGLE_CHIME_SCHEMA)
class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
@@ -34,6 +50,15 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
self._display = ""
self._name = "Alarm Panel"
self._state = None
self._ac_power = None
self._backlight_on = None
self._battery_low = None
self._check_zone = None
self._chime = None
self._entry_delay_off = None
self._programming_mode = None
self._ready = None
self._zone_bypassed = None
@asyncio.coroutine
def async_added_to_hass(self):
@@ -43,21 +68,25 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
def _message_callback(self, message):
if message.alarm_sounding or message.fire_alarm:
if self._state != STATE_ALARM_TRIGGERED:
self._state = STATE_ALARM_TRIGGERED
self.schedule_update_ha_state()
self._state = STATE_ALARM_TRIGGERED
elif message.armed_away:
if self._state != STATE_ALARM_ARMED_AWAY:
self._state = STATE_ALARM_ARMED_AWAY
self.schedule_update_ha_state()
self._state = STATE_ALARM_ARMED_AWAY
elif message.armed_home:
if self._state != STATE_ALARM_ARMED_HOME:
self._state = STATE_ALARM_ARMED_HOME
self.schedule_update_ha_state()
self._state = STATE_ALARM_ARMED_HOME
else:
if self._state != STATE_ALARM_DISARMED:
self._state = STATE_ALARM_DISARMED
self.schedule_update_ha_state()
self._state = STATE_ALARM_DISARMED
self._ac_power = message.ac_power
self._backlight_on = message.backlight_on
self._battery_low = message.battery_low
self._check_zone = message.check_zone
self._chime = message.chime_on
self._entry_delay_off = message.entry_delay_off
self._programming_mode = message.programming_mode
self._ready = message.ready
self._zone_bypassed = message.zone_bypassed
self.schedule_update_ha_state()
@property
def name(self):
@@ -79,20 +108,37 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
"""Return the state of the device."""
return self._state
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
'ac_power': self._ac_power,
'backlight_on': self._backlight_on,
'battery_low': self._battery_low,
'check_zone': self._check_zone,
'chime': self._chime,
'entry_delay_off': self._entry_delay_off,
'programming_mode': self._programming_mode,
'ready': self._ready,
'zone_bypassed': self._zone_bypassed
}
def alarm_disarm(self, code=None):
"""Send disarm command."""
if code:
_LOGGER.debug("alarm_disarm: sending %s1", str(code))
self.hass.data[DATA_AD].send("{!s}1".format(code))
def alarm_arm_away(self, code=None):
"""Send arm away command."""
if code:
_LOGGER.debug("alarm_arm_away: sending %s2", str(code))
self.hass.data[DATA_AD].send("{!s}2".format(code))
def alarm_arm_home(self, code=None):
"""Send arm home command."""
if code:
_LOGGER.debug("alarm_arm_home: sending %s3", str(code))
self.hass.data[DATA_AD].send("{!s}3".format(code))
def alarm_toggle_chime(self, code=None):
"""Send toggle chime command."""
if code:
self.hass.data[DATA_AD].send("{!s}9".format(code))
+2 -2
View File
@@ -18,7 +18,7 @@ from homeassistant.const import (
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['concord232==0.14']
REQUIREMENTS = ['concord232==0.15']
_LOGGER = logging.getLogger(__name__)
@@ -121,4 +121,4 @@ class Concord232Alarm(alarm.AlarmControlPanel):
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self._alarm.arm('auto')
self._alarm.arm('away')
@@ -16,9 +16,9 @@ from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_PORT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, STATE_UNKNOWN,
CONF_NAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED)
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED, EVENT_HOMEASSISTANT_STOP)
REQUIREMENTS = ['pythonegardia==1.0.22']
REQUIREMENTS = ['pythonegardia==1.0.26']
_LOGGER = logging.getLogger(__name__)
@@ -26,13 +26,15 @@ CONF_REPORT_SERVER_CODES = 'report_server_codes'
CONF_REPORT_SERVER_ENABLED = 'report_server_enabled'
CONF_REPORT_SERVER_PORT = 'report_server_port'
CONF_REPORT_SERVER_CODES_IGNORE = 'ignore'
CONF_VERSION = 'version'
DEFAULT_NAME = 'Egardia'
DEFAULT_PORT = 80
DEFAULT_REPORT_SERVER_ENABLED = False
DEFAULT_REPORT_SERVER_PORT = 52010
DEFAULT_VERSION = 'GATE-01'
DOMAIN = 'egardia'
D_EGARDIASRV = 'egardiaserver'
NOTIFICATION_ID = 'egardia_notification'
NOTIFICATION_TITLE = 'Egardia'
@@ -49,6 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_REPORT_SERVER_CODES): vol.All(cv.ensure_list),
@@ -62,6 +65,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Egardia platform."""
from pythonegardia import egardiadevice
from pythonegardia import egardiaserver
name = config.get(CONF_NAME)
username = config.get(CONF_USERNAME)
@@ -71,41 +75,62 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
rs_enabled = config.get(CONF_REPORT_SERVER_ENABLED)
rs_port = config.get(CONF_REPORT_SERVER_PORT)
rs_codes = config.get(CONF_REPORT_SERVER_CODES)
version = config.get(CONF_VERSION)
try:
egardiasystem = egardiadevice.EgardiaDevice(
host, port, username, password, '')
host, port, username, password, '', version)
except requests.exceptions.RequestException:
raise exc.PlatformNotReady()
except egardiadevice.UnauthorizedError:
_LOGGER.error("Unable to authorize. Wrong password or username")
return False
return
add_devices([EgardiaAlarm(
name, egardiasystem, hass, rs_enabled, rs_port, rs_codes)], True)
eg_dev = EgardiaAlarm(
name, egardiasystem, rs_enabled, rs_codes)
if rs_enabled:
# Set up the egardia server
_LOGGER.info("Setting up EgardiaServer")
try:
if D_EGARDIASRV not in hass.data:
server = egardiaserver.EgardiaServer('', rs_port)
bound = server.bind()
if not bound:
raise IOError("Binding error occurred while " +
"starting EgardiaServer")
hass.data[D_EGARDIASRV] = server
server.start()
except IOError:
return
hass.data[D_EGARDIASRV].register_callback(eg_dev.handle_status_event)
def handle_stop_event(event):
"""Callback function for HA stop event."""
hass.data[D_EGARDIASRV].stop()
# listen to home assistant stop event
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop_event)
# add egardia alarm device
add_devices([eg_dev], True)
class EgardiaAlarm(alarm.AlarmControlPanel):
"""Representation of a Egardia alarm."""
def __init__(self, name, egardiasystem, hass, rs_enabled=False,
rs_port=None, rs_codes=None):
def __init__(self, name, egardiasystem,
rs_enabled=False, rs_codes=None):
"""Initialize object."""
self._name = name
self._egardiasystem = egardiasystem
self._status = STATE_UNKNOWN
self._status = None
self._rs_enabled = rs_enabled
self._rs_port = rs_port
self._hass = hass
if rs_codes is not None:
self._rs_codes = rs_codes[0]
else:
self._rs_codes = rs_codes
if self._rs_enabled:
self.listen_to_system_status()
@property
def name(self):
"""Return the name of the device."""
@@ -123,19 +148,14 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
return True
return False
def handle_system_status_event(self, event):
def handle_status_event(self, event):
"""Handle egardia_system_status_event."""
if event.data.get('status') is not None:
statuscode = event.data.get('status')
statuscode = event.get('status')
if statuscode is not None:
status = self.lookupstatusfromcode(statuscode)
self.parsestatus(status)
self.schedule_update_ha_state()
def listen_to_system_status(self):
"""Subscribe to egardia_system_status event."""
self._hass.bus.listen(
'egardia_system_status', self.handle_system_status_event)
def lookupstatusfromcode(self, statuscode):
"""Look at the rs_codes and returns the status from the code."""
status = 'UNKNOWN'
@@ -6,7 +6,6 @@ https://home-assistant.io/components/alarm_control_panel.envisalink/
"""
import asyncio
import logging
import os
import voluptuous as vol
@@ -14,7 +13,6 @@ from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.components.alarm_control_panel as alarm
import homeassistant.helpers.config_validation as cv
from homeassistant.config import load_yaml_config_file
from homeassistant.components.envisalink import (
DATA_EVL, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
CONF_PARTITIONNAME, SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE)
@@ -69,14 +67,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
for device in target_devices:
device.async_alarm_keypress(keypress)
# Register Envisalink specific services
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
hass.services.async_register(
alarm.DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler,
descriptions.get(SERVICE_ALARM_KEYPRESS), schema=ALARM_KEYPRESS_SCHEMA)
schema=ALARM_KEYPRESS_SCHEMA)
return True
@@ -17,7 +17,9 @@ from homeassistant.const import (
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
CONF_NAME, CONF_CODE)
from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS)
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
MqttAvailability)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -54,15 +56,21 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
config.get(CONF_PAYLOAD_DISARM),
config.get(CONF_PAYLOAD_ARM_HOME),
config.get(CONF_PAYLOAD_ARM_AWAY),
config.get(CONF_CODE))])
config.get(CONF_CODE),
config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE))])
class MqttAlarm(alarm.AlarmControlPanel):
class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
"""Representation of a MQTT alarm status."""
def __init__(self, name, state_topic, command_topic, qos, payload_disarm,
payload_arm_home, payload_arm_away, code):
payload_arm_home, payload_arm_away, code, availability_topic,
payload_available, payload_not_available):
"""Init the MQTT Alarm Control Panel."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
self._state = STATE_UNKNOWN
self._name = name
self._state_topic = state_topic
@@ -73,11 +81,11 @@ class MqttAlarm(alarm.AlarmControlPanel):
self._payload_arm_away = payload_arm_away
self._code = code
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe mqtt events.
"""Subscribe mqtt events."""
yield from super().async_added_to_hass()
This method must be run in the event loop and returns a coroutine.
"""
@callback
def message_received(topic, payload, qos):
"""Run when new MQTT message has been received."""
@@ -89,7 +97,7 @@ class MqttAlarm(alarm.AlarmControlPanel):
self._state = payload
self.async_schedule_update_ha_state()
return mqtt.async_subscribe(
yield from mqtt.async_subscribe(
self.hass, self._state_topic, message_received, self._qos)
@property
@@ -59,3 +59,13 @@ envisalink_alarm_keypress:
keypress:
description: 'String to send to the alarm panel (1-6 characters).'
example: '*71'
alarmdecoder_alarm_toggle_chime:
description: Send the alarm the toggle chime command.
fields:
entity_id:
description: Name of the alarm control panel to trigger.
example: 'alarm_control_panel.downstairs'
code:
description: A required code to toggle the alarm control panel chime with.
example: 1234
+43 -4
View File
@@ -6,13 +6,16 @@ https://home-assistant.io/components/alarmdecoder/
"""
import logging
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.discovery import load_platform
from homeassistant.util import dt as dt_util
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
REQUIREMENTS = ['alarmdecoder==0.12.3']
REQUIREMENTS = ['alarmdecoder==1.13.2']
_LOGGER = logging.getLogger(__name__)
@@ -29,6 +32,7 @@ CONF_DEVICE_TYPE = 'type'
CONF_PANEL_DISPLAY = 'panel_display'
CONF_ZONE_NAME = 'name'
CONF_ZONE_TYPE = 'type'
CONF_ZONE_RFID = 'rfid'
CONF_ZONES = 'zones'
DEFAULT_DEVICE_TYPE = 'socket'
@@ -48,6 +52,7 @@ SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm'
SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault'
SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore'
SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message'
DEVICE_SOCKET_SCHEMA = vol.Schema({
vol.Required(CONF_DEVICE_TYPE): 'socket',
@@ -64,7 +69,9 @@ DEVICE_USB_SCHEMA = vol.Schema({
ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_ZONE_NAME): cv.string,
vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): cv.string})
vol.Optional(CONF_ZONE_TYPE,
default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA),
vol.Optional(CONF_ZONE_RFID): cv.string})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@@ -85,6 +92,7 @@ def setup(hass, config):
conf = config.get(DOMAIN)
restart = False
device = conf.get(CONF_DEVICE)
display = conf.get(CONF_PANEL_DISPLAY)
zones = conf.get(CONF_ZONES)
@@ -98,13 +106,43 @@ def setup(hass, config):
def stop_alarmdecoder(event):
"""Handle the shutdown of AlarmDecoder."""
_LOGGER.debug("Shutting down alarmdecoder")
nonlocal restart
restart = False
controller.close()
def open_connection(now=None):
"""Open a connection to AlarmDecoder."""
from alarmdecoder.util import NoDeviceError
nonlocal restart
try:
controller.open(baud)
except NoDeviceError:
_LOGGER.debug("Failed to connect. Retrying in 5 seconds")
hass.helpers.event.track_point_in_time(
open_connection, dt_util.utcnow() + timedelta(seconds=5))
return
_LOGGER.debug("Established a connection with the alarmdecoder")
restart = True
def handle_closed_connection(event):
"""Restart after unexpected loss of connection."""
nonlocal restart
if not restart:
return
restart = False
_LOGGER.warning("AlarmDecoder unexpectedly lost connection.")
hass.add_job(open_connection)
def handle_message(sender, message):
"""Handle message from AlarmDecoder."""
hass.helpers.dispatcher.dispatcher_send(
SIGNAL_PANEL_MESSAGE, message)
def handle_rfx_message(sender, message):
"""Handle RFX message from AlarmDecoder."""
hass.helpers.dispatcher.dispatcher_send(
SIGNAL_RFX_MESSAGE, message)
def zone_fault_callback(sender, zone):
"""Handle zone fault from AlarmDecoder."""
hass.helpers.dispatcher.dispatcher_send(
@@ -129,14 +167,15 @@ def setup(hass, config):
return False
controller.on_message += handle_message
controller.on_rfx_message += handle_rfx_message
controller.on_zone_fault += zone_fault_callback
controller.on_zone_restore += zone_restore_callback
controller.on_close += handle_closed_connection
hass.data[DATA_AD] = controller
controller.open(baud)
open_connection()
_LOGGER.debug("Established a connection with the alarmdecoder")
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config)
+3 -11
View File
@@ -7,12 +7,10 @@ https://home-assistant.io/components/alert/
import asyncio
from datetime import datetime, timedelta
import logging
import os
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF,
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
@@ -129,22 +127,16 @@ def async_setup(hass, config):
alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK])
all_alerts[entity.entity_id] = entity
# Read descriptions
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
descriptions = descriptions.get(DOMAIN, {})
# Setup service calls
hass.services.async_register(
DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service,
descriptions.get(SERVICE_TURN_OFF), schema=ALERT_SERVICE_SCHEMA)
schema=ALERT_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_TURN_ON, async_handle_alert_service,
descriptions.get(SERVICE_TURN_ON), schema=ALERT_SERVICE_SCHEMA)
schema=ALERT_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_TOGGLE, async_handle_alert_service,
descriptions.get(SERVICE_TOGGLE), schema=ALERT_SERVICE_SCHEMA)
schema=ALERT_SERVICE_SCHEMA)
tasks = [alert.async_update_ha_state() for alert in all_alerts.values()]
if tasks:
+103 -57
View File
@@ -9,15 +9,16 @@ import asyncio
import enum
import logging
from homeassistant.exceptions import HomeAssistantError
from homeassistant.core import callback
from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import intent
from homeassistant.components import http
from homeassistant.util.decorator import Registry
from .const import DOMAIN, SYN_RESOLUTION_MATCH
INTENTS_API_ENDPOINT = '/api/alexa'
HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__)
@@ -47,6 +48,10 @@ def async_setup(hass):
hass.http.register_view(AlexaIntentsView)
class UnknownRequest(HomeAssistantError):
"""When an unknown Alexa request is passed in."""
class AlexaIntentsView(http.HomeAssistantView):
"""Handle Alexa requests."""
@@ -57,71 +62,112 @@ class AlexaIntentsView(http.HomeAssistantView):
def post(self, request):
"""Handle Alexa."""
hass = request.app['hass']
data = yield from request.json()
message = yield from request.json()
_LOGGER.debug('Received Alexa request: %s', data)
req = data.get('request')
if req is None:
_LOGGER.error('Received invalid data from Alexa: %s', data)
return self.json_message('Expected request value not received',
HTTP_BAD_REQUEST)
req_type = req['type']
if req_type == 'SessionEndedRequest':
return None
alexa_intent_info = req.get('intent')
alexa_response = AlexaResponse(hass, alexa_intent_info)
if req_type != 'IntentRequest' and req_type != 'LaunchRequest':
_LOGGER.warning('Received unsupported request: %s', req_type)
return self.json_message(
'Received unsupported request: {}'.format(req_type),
HTTP_BAD_REQUEST)
if req_type == 'LaunchRequest':
intent_name = data.get('session', {}) \
.get('application', {}) \
.get('applicationId')
else:
intent_name = alexa_intent_info['name']
_LOGGER.debug('Received Alexa request: %s', message)
try:
intent_response = yield from intent.async_handle(
hass, DOMAIN, intent_name,
{key: {'value': value} for key, value
in alexa_response.variables.items()})
response = yield from async_handle_message(hass, message)
return b'' if response is None else self.json(response)
except UnknownRequest as err:
_LOGGER.warning(str(err))
return self.json(intent_error_response(
hass, message, str(err)))
except intent.UnknownIntent as err:
_LOGGER.warning('Received unknown intent %s', intent_name)
alexa_response.add_speech(
SpeechType.plaintext,
"This intent is not yet configured within Home Assistant.")
return self.json(alexa_response)
_LOGGER.warning(str(err))
return self.json(intent_error_response(
hass, message,
"This intent is not yet configured within Home Assistant."))
except intent.InvalidSlotInfo as err:
_LOGGER.error('Received invalid slot data from Alexa: %s', err)
return self.json_message('Invalid slot data received',
HTTP_BAD_REQUEST)
except intent.IntentError:
_LOGGER.exception('Error handling request for %s', intent_name)
return self.json_message('Error handling intent', HTTP_BAD_REQUEST)
return self.json(intent_error_response(
hass, message,
"Invalid slot information received for this intent."))
for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
if intent_speech in intent_response.speech:
alexa_response.add_speech(
alexa_speech,
intent_response.speech[intent_speech]['speech'])
break
except intent.IntentError as err:
_LOGGER.exception(str(err))
return self.json(intent_error_response(
hass, message, "Error handling intent."))
if 'simple' in intent_response.card:
alexa_response.add_card(
CardType.simple, intent_response.card['simple']['title'],
intent_response.card['simple']['content'])
return self.json(alexa_response)
def intent_error_response(hass, message, error):
"""Return an Alexa response that will speak the error message."""
alexa_intent_info = message.get('request').get('intent')
alexa_response = AlexaResponse(hass, alexa_intent_info)
alexa_response.add_speech(SpeechType.plaintext, error)
return alexa_response.as_dict()
@asyncio.coroutine
def async_handle_message(hass, message):
"""Handle an Alexa intent.
Raises:
- UnknownRequest
- intent.UnknownIntent
- intent.InvalidSlotInfo
- intent.IntentError
"""
req = message.get('request')
req_type = req['type']
handler = HANDLERS.get(req_type)
if not handler:
raise UnknownRequest('Received unknown request {}'.format(req_type))
return (yield from handler(hass, message))
@HANDLERS.register('SessionEndedRequest')
@asyncio.coroutine
def async_handle_session_end(hass, message):
"""Handle a session end request."""
return None
@HANDLERS.register('IntentRequest')
@HANDLERS.register('LaunchRequest')
@asyncio.coroutine
def async_handle_intent(hass, message):
"""Handle an intent request.
Raises:
- intent.UnknownIntent
- intent.InvalidSlotInfo
- intent.IntentError
"""
req = message.get('request')
alexa_intent_info = req.get('intent')
alexa_response = AlexaResponse(hass, alexa_intent_info)
if req['type'] == 'LaunchRequest':
intent_name = message.get('session', {}) \
.get('application', {}) \
.get('applicationId')
else:
intent_name = alexa_intent_info['name']
intent_response = yield from intent.async_handle(
hass, DOMAIN, intent_name,
{key: {'value': value} for key, value
in alexa_response.variables.items()})
for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
if intent_speech in intent_response.speech:
alexa_response.add_speech(
alexa_speech,
intent_response.speech[intent_speech]['speech'])
break
if 'simple' in intent_response.card:
alexa_response.add_card(
CardType.simple, intent_response.card['simple']['title'],
intent_response.card['simple']['content'])
return alexa_response.as_dict()
def resolve_slot_synonyms(key, request):
+67 -55
View File
@@ -1,6 +1,5 @@
"""Support for alexa Smart Home Skill API."""
import asyncio
from collections import namedtuple
import logging
import math
from uuid import uuid4
@@ -27,10 +26,9 @@ API_EVENT = 'event'
API_HEADER = 'header'
API_PAYLOAD = 'payload'
ATTR_ALEXA_DESCRIPTION = 'alexa_description'
ATTR_ALEXA_DISPLAY_CATEGORIES = 'alexa_display_categories'
ATTR_ALEXA_HIDDEN = 'alexa_hidden'
ATTR_ALEXA_NAME = 'alexa_name'
CONF_DESCRIPTION = 'description'
CONF_DISPLAY_CATEGORIES = 'display_categories'
CONF_NAME = 'name'
MAPPING_COMPONENT = {
@@ -73,7 +71,13 @@ MAPPING_COMPONENT = {
}
Config = namedtuple('AlexaConfig', 'filter')
class Config:
"""Hold the configuration for Alexa."""
def __init__(self, should_expose, entity_config=None):
"""Initialize the configuration."""
self.should_expose = should_expose
self.entity_config = entity_config or {}
@asyncio.coroutine
@@ -150,32 +154,28 @@ def async_api_discovery(hass, config, request):
discovery_endpoints = []
for entity in hass.states.async_all():
if not config.filter(entity.entity_id):
if not config.should_expose(entity.entity_id):
_LOGGER.debug("Not exposing %s because filtered by config",
entity.entity_id)
continue
if entity.attributes.get(ATTR_ALEXA_HIDDEN, False):
_LOGGER.debug("Not exposing %s because alexa_hidden is true",
entity.entity_id)
continue
class_data = MAPPING_COMPONENT.get(entity.domain)
if not class_data:
continue
friendly_name = entity.attributes.get(ATTR_ALEXA_NAME, entity.name)
description = entity.attributes.get(ATTR_ALEXA_DESCRIPTION,
entity.entity_id)
entity_conf = config.entity_config.get(entity.entity_id, {})
friendly_name = entity_conf.get(CONF_NAME, entity.name)
description = entity_conf.get(CONF_DESCRIPTION, entity.entity_id)
# Required description as per Amazon Scene docs
if entity.domain == scene.DOMAIN:
scene_fmt = '{} (Scene connected via Home Assistant)'
description = scene_fmt.format(description)
cat_key = ATTR_ALEXA_DISPLAY_CATEGORIES
display_categories = entity.attributes.get(cat_key, class_data[0])
display_categories = entity_conf.get(CONF_DISPLAY_CATEGORIES,
class_data[0])
endpoint = {
'displayCategories': [display_categories],
@@ -243,9 +243,13 @@ def async_api_turn_on(hass, config, request, entity):
if entity.domain == group.DOMAIN:
domain = ha.DOMAIN
yield from hass.services.async_call(domain, SERVICE_TURN_ON, {
service = SERVICE_TURN_ON
if entity.domain == cover.DOMAIN:
service = cover.SERVICE_OPEN_COVER
yield from hass.services.async_call(domain, service, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=True)
}, blocking=False)
return api_message(request)
@@ -259,9 +263,13 @@ def async_api_turn_off(hass, config, request, entity):
if entity.domain == group.DOMAIN:
domain = ha.DOMAIN
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, {
service = SERVICE_TURN_OFF
if entity.domain == cover.DOMAIN:
service = cover.SERVICE_CLOSE_COVER
yield from hass.services.async_call(domain, service, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=True)
}, blocking=False)
return api_message(request)
@@ -276,7 +284,7 @@ def async_api_set_brightness(hass, config, request, entity):
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_BRIGHTNESS_PCT: brightness,
}, blocking=True)
}, blocking=False)
return api_message(request)
@@ -300,7 +308,7 @@ def async_api_adjust_brightness(hass, config, request, entity):
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_BRIGHTNESS_PCT: brightness,
}, blocking=True)
}, blocking=False)
return api_message(request)
@@ -321,14 +329,14 @@ def async_api_set_color(hass, config, request, entity):
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_RGB_COLOR: rgb,
}, blocking=True)
}, blocking=False)
else:
xyz = color_util.color_RGB_to_xy(*rgb)
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_XY_COLOR: (xyz[0], xyz[1]),
light.ATTR_BRIGHTNESS: xyz[2],
}, blocking=True)
}, blocking=False)
return api_message(request)
@@ -343,7 +351,7 @@ def async_api_set_color_temperature(hass, config, request, entity):
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_KELVIN: kelvin,
}, blocking=True)
}, blocking=False)
return api_message(request)
@@ -361,7 +369,7 @@ def async_api_decrease_color_temp(hass, config, request, entity):
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_COLOR_TEMP: value,
}, blocking=True)
}, blocking=False)
return api_message(request)
@@ -379,7 +387,7 @@ def async_api_increase_color_temp(hass, config, request, entity):
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_COLOR_TEMP: value,
}, blocking=True)
}, blocking=False)
return api_message(request)
@@ -391,7 +399,7 @@ def async_api_activate(hass, config, request, entity):
"""Process a activate request."""
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=True)
}, blocking=False)
return api_message(request)
@@ -421,8 +429,8 @@ def async_api_set_percentage(hass, config, request, entity):
service = SERVICE_SET_COVER_POSITION
data[cover.ATTR_POSITION] = percentage
yield from hass.services.async_call(entity.domain, service,
data, blocking=True)
yield from hass.services.async_call(
entity.domain, service, data, blocking=False)
return api_message(request)
@@ -469,8 +477,8 @@ def async_api_adjust_percentage(hass, config, request, entity):
data[cover.ATTR_POSITION] = max(0, percentage_delta + current)
yield from hass.services.async_call(entity.domain, service,
data, blocking=True)
yield from hass.services.async_call(
entity.domain, service, data, blocking=False)
return api_message(request)
@@ -482,7 +490,7 @@ def async_api_lock(hass, config, request, entity):
"""Process a lock request."""
yield from hass.services.async_call(entity.domain, SERVICE_LOCK, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=True)
}, blocking=False)
return api_message(request)
@@ -495,7 +503,7 @@ def async_api_unlock(hass, config, request, entity):
"""Process a unlock request."""
yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=True)
}, blocking=False)
return api_message(request)
@@ -512,8 +520,9 @@ def async_api_set_volume(hass, config, request, entity):
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
}
yield from hass.services.async_call(entity.domain, SERVICE_VOLUME_SET,
data, blocking=True)
yield from hass.services.async_call(
entity.domain, SERVICE_VOLUME_SET,
data, blocking=False)
return api_message(request)
@@ -540,9 +549,9 @@ def async_api_adjust_volume(hass, config, request, entity):
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
}
yield from hass.services.async_call(entity.domain,
media_player.SERVICE_VOLUME_SET,
data, blocking=True)
yield from hass.services.async_call(
entity.domain, media_player.SERVICE_VOLUME_SET,
data, blocking=False)
return api_message(request)
@@ -559,9 +568,9 @@ def async_api_set_mute(hass, config, request, entity):
media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
}
yield from hass.services.async_call(entity.domain,
media_player.SERVICE_VOLUME_MUTE,
data, blocking=True)
yield from hass.services.async_call(
entity.domain, media_player.SERVICE_VOLUME_MUTE,
data, blocking=False)
return api_message(request)
@@ -575,8 +584,9 @@ def async_api_play(hass, config, request, entity):
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PLAY,
data, blocking=True)
yield from hass.services.async_call(
entity.domain, SERVICE_MEDIA_PLAY,
data, blocking=False)
return api_message(request)
@@ -590,8 +600,9 @@ def async_api_pause(hass, config, request, entity):
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PAUSE,
data, blocking=True)
yield from hass.services.async_call(
entity.domain, SERVICE_MEDIA_PAUSE,
data, blocking=False)
return api_message(request)
@@ -605,8 +616,9 @@ def async_api_stop(hass, config, request, entity):
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_STOP,
data, blocking=True)
yield from hass.services.async_call(
entity.domain, SERVICE_MEDIA_STOP,
data, blocking=False)
return api_message(request)
@@ -620,9 +632,9 @@ def async_api_next(hass, config, request, entity):
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(entity.domain,
SERVICE_MEDIA_NEXT_TRACK,
data, blocking=True)
yield from hass.services.async_call(
entity.domain, SERVICE_MEDIA_NEXT_TRACK,
data, blocking=False)
return api_message(request)
@@ -636,8 +648,8 @@ def async_api_previous(hass, config, request, entity):
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(entity.domain,
SERVICE_MEDIA_PREVIOUS_TRACK,
data, blocking=True)
yield from hass.services.async_call(
entity.domain, SERVICE_MEDIA_PREVIOUS_TRACK,
data, blocking=False)
return api_message(request)
+7 -3
View File
@@ -24,6 +24,7 @@ from homeassistant.const import (
__version__)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.state import AsyncTrackStates
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers import template
from homeassistant.components.http import HomeAssistantView
@@ -293,10 +294,11 @@ class APIServicesView(HomeAssistantView):
url = URL_API_SERVICES
name = "api:services"
@ha.callback
@asyncio.coroutine
def get(self, request):
"""Get registered services."""
return self.json(async_services_json(request.app['hass']))
services = yield from async_services_json(request.app['hass'])
return self.json(services)
class APIDomainServicesView(HomeAssistantView):
@@ -355,10 +357,12 @@ class APITemplateView(HomeAssistantView):
HTTP_BAD_REQUEST)
@asyncio.coroutine
def async_services_json(hass):
"""Generate services data to JSONify."""
descriptions = yield from async_get_all_descriptions(hass)
return [{"domain": key, "services": value}
for key, value in hass.services.async_services().items()]
for key, value in descriptions.items()]
def async_events_json(hass):
-8
View File
@@ -4,7 +4,6 @@ Support for Apple TV.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/apple_tv/
"""
import os
import asyncio
import logging
@@ -12,7 +11,6 @@ import voluptuous as vol
from typing import Union, TypeVar, Sequence
from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_ENTITY_ID)
from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import discovery
from homeassistant.components.discovery import SERVICE_APPLE_TV
@@ -183,18 +181,12 @@ def async_setup(hass, config):
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
hass.services.async_register(
DOMAIN, SERVICE_SCAN, async_service_handler,
descriptions.get(SERVICE_SCAN),
schema=APPLE_TV_SCAN_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_AUTHENTICATE, async_service_handler,
descriptions.get(SERVICE_AUTHENTICATE),
schema=APPLE_TV_AUTHENTICATE_SCHEMA)
return True
+1 -1
View File
@@ -12,7 +12,7 @@ from requests.exceptions import HTTPError, ConnectTimeout
from homeassistant.helpers import config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
REQUIREMENTS = ['pyarlo==0.1.0']
REQUIREMENTS = ['pyarlo==0.1.2']
_LOGGER = logging.getLogger(__name__)
@@ -7,14 +7,12 @@ https://home-assistant.io/components/automation/
import asyncio
from functools import partial
import logging
import os
import voluptuous as vol
from homeassistant.setup import async_prepare_setup_platform
from homeassistant.core import CoreState
from homeassistant.loader import bind_hass
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, CONF_ID)
@@ -166,11 +164,6 @@ def async_setup(hass, config):
yield from _async_process_config(hass, config, component)
descriptions = yield from hass.async_add_job(
conf_util.load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml')
)
@asyncio.coroutine
def trigger_service_handler(service_call):
"""Handle automation triggers."""
@@ -216,20 +209,20 @@ def async_setup(hass, config):
hass.services.async_register(
DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
descriptions.get(SERVICE_TRIGGER), schema=TRIGGER_SERVICE_SCHEMA)
schema=TRIGGER_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_RELOAD, reload_service_handler,
descriptions.get(SERVICE_RELOAD), schema=RELOAD_SERVICE_SCHEMA)
schema=RELOAD_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
descriptions.get(SERVICE_TOGGLE), schema=SERVICE_SCHEMA)
schema=SERVICE_SCHEMA)
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
hass.services.async_register(
DOMAIN, service, turn_onoff_service_handler,
descriptions.get(service), schema=SERVICE_SCHEMA)
schema=SERVICE_SCHEMA)
return True
+1 -1
View File
@@ -55,7 +55,7 @@ def async_trigger(hass, config, action):
# Ignore changes to state attributes if from/to is in use
if (not match_all and from_s is not None and to_s is not None and
from_s.last_changed == to_s.last_changed):
from_s.state == to_s.state):
return
if not time_delta:
-7
View File
@@ -6,12 +6,10 @@ https://home-assistant.io/components/axis/
"""
import logging
import os
import voluptuous as vol
from homeassistant.components.discovery import SERVICE_AXIS
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
CONF_EVENT, CONF_HOST, CONF_INCLUDE,
CONF_NAME, CONF_PASSWORD, CONF_PORT,
@@ -195,10 +193,6 @@ def setup(hass, config):
if not setup_device(hass, config, device_config):
_LOGGER.error("Couldn\'t set up %s", device_config[CONF_NAME])
# Services to communicate with device.
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
def vapix_service(call):
"""Service to send a message."""
for _, device in AXIS_DEVICES.items():
@@ -216,7 +210,6 @@ def setup(hass, config):
hass.services.register(DOMAIN,
SERVICE_VAPIX_CALL,
vapix_service,
descriptions[DOMAIN][SERVICE_VAPIX_CALL],
schema=SERVICE_SCHEMA)
return True
@@ -21,24 +21,27 @@ SCAN_INTERVAL = timedelta(seconds=30)
ENTITY_ID_FORMAT = DOMAIN + '.{}'
DEVICE_CLASSES = [
'battery', # On means low, Off means normal
'cold', # On means cold (or too cold)
'connectivity', # On means connection present, Off = no connection
'gas', # CO, CO2, etc.
'heat', # On means hot (or too hot)
'light', # Lightness threshold
'moisture', # Specifically a wetness sensor
'motion', # Motion sensor
'moving', # On means moving, Off means stopped
'occupancy', # On means occupied, Off means not occupied
'opening', # Door, window, etc.
'cold', # On means cold, Off means normal
'connectivity', # On means connected, Off means disconnected
'door', # On means open, Off means closed
'garage_door', # On means open, Off means closed
'gas', # On means gas detected, Off means no gas (clear)
'heat', # On means hot, Off means normal
'light', # On means light detected, Off means no light
'moisture', # On means wet, Off means dry
'motion', # On means motion detected, Off means no motion (clear)
'moving', # On means moving, Off means not moving (stopped)
'occupancy', # On means occupied, Off means not occupied (clear)
'opening', # On means open, Off means closed
'plug', # On means plugged in, Off means unplugged
'power', # Power, over-current, etc
'power', # On means power detected, Off means no power
'presence', # On means home, Off means away
'problem', # On means there is a problem, Off means the status is OK
'safety', # Generic on=unsafe, off=safe
'smoke', # Smoke detector
'sound', # On means sound detected, Off means no sound
'problem', # On means problem detected, Off means no problem (OK)
'safety', # On means unsafe, Off means safe
'smoke', # On means smoke detected, Off means no smoke (clear)
'sound', # On means sound detected, Off means no sound (clear)
'vibration', # On means vibration detected, Off means no vibration
'window', # On means open, Off means closed
]
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
@@ -10,12 +10,22 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.alarmdecoder import (
ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE)
CONF_ZONE_RFID, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE,
SIGNAL_RFX_MESSAGE)
DEPENDENCIES = ['alarmdecoder']
_LOGGER = logging.getLogger(__name__)
ATTR_RF_BIT0 = 'rf_bit0'
ATTR_RF_LOW_BAT = 'rf_low_battery'
ATTR_RF_SUPERVISED = 'rf_supervised'
ATTR_RF_BIT3 = 'rf_bit3'
ATTR_RF_LOOP3 = 'rf_loop3'
ATTR_RF_LOOP2 = 'rf_loop2'
ATTR_RF_LOOP4 = 'rf_loop4'
ATTR_RF_LOOP1 = 'rf_loop1'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the AlarmDecoder binary sensor devices."""
@@ -26,7 +36,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
device_config_data = ZONE_SCHEMA(configured_zones[zone_num])
zone_type = device_config_data[CONF_ZONE_TYPE]
zone_name = device_config_data[CONF_ZONE_NAME]
device = AlarmDecoderBinarySensor(zone_num, zone_name, zone_type)
zone_rfid = device_config_data.get(CONF_ZONE_RFID)
device = AlarmDecoderBinarySensor(
zone_num, zone_name, zone_type, zone_rfid)
devices.append(device)
add_devices(devices)
@@ -37,13 +49,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class AlarmDecoderBinarySensor(BinarySensorDevice):
"""Representation of an AlarmDecoder binary sensor."""
def __init__(self, zone_number, zone_name, zone_type):
def __init__(self, zone_number, zone_name, zone_type, zone_rfid):
"""Initialize the binary_sensor."""
self._zone_number = zone_number
self._zone_type = zone_type
self._state = 0
self._state = None
self._name = zone_name
self._type = zone_type
self._rfid = zone_rfid
self._rfstate = None
@asyncio.coroutine
def async_added_to_hass(self):
@@ -54,27 +67,34 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_ZONE_RESTORE, self._restore_callback)
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_RFX_MESSAGE, self._rfx_message_callback)
@property
def name(self):
"""Return the name of the entity."""
return self._name
@property
def icon(self):
"""Icon for device by its type."""
if "window" in self._name.lower():
return "mdi:window-open" if self.is_on else "mdi:window-closed"
if self._type == 'smoke':
return "mdi:fire"
return None
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def device_state_attributes(self):
"""Return the state attributes."""
attr = {}
if self._rfid and self._rfstate is not None:
attr[ATTR_RF_BIT0] = True if self._rfstate & 0x01 else False
attr[ATTR_RF_LOW_BAT] = True if self._rfstate & 0x02 else False
attr[ATTR_RF_SUPERVISED] = True if self._rfstate & 0x04 else False
attr[ATTR_RF_BIT3] = True if self._rfstate & 0x08 else False
attr[ATTR_RF_LOOP3] = True if self._rfstate & 0x10 else False
attr[ATTR_RF_LOOP2] = True if self._rfstate & 0x20 else False
attr[ATTR_RF_LOOP4] = True if self._rfstate & 0x40 else False
attr[ATTR_RF_LOOP1] = True if self._rfstate & 0x80 else False
return attr
@property
def is_on(self):
"""Return true if sensor is on."""
@@ -96,3 +116,9 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
if zone is None or int(zone) == self._zone_number:
self._state = 0
self.schedule_update_ha_state()
def _rfx_message_callback(self, message):
"""Update RF state."""
if self._rfid and message and message.serial_number == self._rfid:
self._rfstate = message.value
self.schedule_update_ha_state()
+9 -2
View File
@@ -15,7 +15,7 @@ from homeassistant.components.binary_sensor import (
from homeassistant.const import (CONF_HOST, CONF_PORT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['concord232==0.14']
REQUIREMENTS = ['concord232==0.15']
_LOGGER = logging.getLogger(__name__)
@@ -62,6 +62,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
return False
# The order of zones returned by client.list_zones() can vary.
# When the zones are not named, this can result in the same entity
# name mapping to different sensors in an unpredictable way. Sort
# the zones by zone number to prevent this.
client.zones.sort(key=lambda zone: zone['number'])
for zone in client.zones:
_LOGGER.info("Loading Zone found: %s", zone['name'])
if zone['number'] not in exclude:
@@ -118,7 +125,7 @@ class Concord232ZoneSensor(BinarySensorDevice):
def is_on(self):
"""Return true if the binary sensor is on."""
# True means "faulted" or "open" or "abnormal state"
return bool(self._zone['state'] == 'Normal')
return bool(self._zone['state'] != 'Normal')
def update(self):
"""Get updated stats from API."""
@@ -0,0 +1,97 @@
"""
Support for deCONZ binary sensor.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.deconz/
"""
import asyncio
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz import DOMAIN as DECONZ_DATA
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback
DEPENDENCIES = ['deconz']
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup binary sensor for deCONZ component."""
if discovery_info is None:
return
from pydeconz.sensor import DECONZ_BINARY_SENSOR
sensors = hass.data[DECONZ_DATA].sensors
entities = []
for sensor in sensors.values():
if sensor.type in DECONZ_BINARY_SENSOR:
entities.append(DeconzBinarySensor(sensor))
async_add_devices(entities, True)
class DeconzBinarySensor(BinarySensorDevice):
"""Representation of a binary sensor."""
def __init__(self, sensor):
"""Setup sensor and add update callback to get data from websocket."""
self._sensor = sensor
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe sensors events."""
self._sensor.register_async_callback(self.async_update_callback)
@callback
def async_update_callback(self, reason):
"""Update the sensor's state.
If reason is that state is updated,
or reachable has changed or battery has changed.
"""
if reason['state'] or \
'reachable' in reason['attr'] or \
'battery' in reason['attr']:
self.async_schedule_update_ha_state()
@property
def is_on(self):
"""Return true if sensor is on."""
return self._sensor.is_tripped
@property
def name(self):
"""Return the name of the sensor."""
return self._sensor.name
@property
def device_class(self):
"""Class of the sensor."""
return self._sensor.sensor_class
@property
def icon(self):
"""Return the icon to use in the frontend."""
return self._sensor.sensor_icon
@property
def available(self):
"""Return True if sensor is available."""
return self._sensor.reachable
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
from pydeconz.sensor import PRESENCE
attr = {
ATTR_BATTERY_LEVEL: self._sensor.battery,
}
if self._sensor.type == PRESENCE:
attr['dark'] = self._sensor.dark
return attr
@@ -1,60 +0,0 @@
"""Support for reading binary states from a DoorBird video doorbell."""
from datetime import timedelta
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.doorbird import DOMAIN as DOORBIRD_DOMAIN
from homeassistant.util import Throttle
DEPENDENCIES = ['doorbird']
_LOGGER = logging.getLogger(__name__)
_MIN_UPDATE_INTERVAL = timedelta(milliseconds=250)
SENSOR_TYPES = {
"doorbell": {
"name": "Doorbell Ringing",
"icon": {
True: "bell-ring",
False: "bell",
None: "bell-outline"
}
}
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the DoorBird binary sensor component."""
device = hass.data.get(DOORBIRD_DOMAIN)
add_devices([DoorBirdBinarySensor(device, "doorbell")], True)
class DoorBirdBinarySensor(BinarySensorDevice):
"""A binary sensor of a DoorBird device."""
def __init__(self, device, sensor_type):
"""Initialize a binary sensor on a DoorBird device."""
self._device = device
self._sensor_type = sensor_type
self._state = None
@property
def name(self):
"""Get the name of the sensor."""
return SENSOR_TYPES[self._sensor_type]["name"]
@property
def icon(self):
"""Get an icon to display."""
state_icon = SENSOR_TYPES[self._sensor_type]["icon"][self._state]
return "mdi:{}".format(state_icon)
@property
def is_on(self):
"""Get the state of the binary sensor."""
return self._state
@Throttle(_MIN_UPDATE_INTERVAL)
def update(self):
"""Pull the latest value from the device."""
self._state = self._device.doorbell_state()
@@ -12,7 +12,8 @@ from typing import Callable # noqa
from homeassistant.core import callback
from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN
import homeassistant.components.isy994 as isy
from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS,
ISYDevice)
from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.event import async_track_point_in_utc_time
@@ -20,9 +21,6 @@ from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
UOM = ['2', '78']
STATES = [STATE_OFF, STATE_ON, 'true', 'false']
ISY_DEVICE_TYPES = {
'moisture': ['16.8', '16.13', '16.14'],
'opening': ['16.9', '16.6', '16.7', '16.2', '16.17', '16.20', '16.21'],
@@ -34,16 +32,11 @@ ISY_DEVICE_TYPES = {
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 binary sensor platform."""
if isy.ISY is None or not isy.ISY.connected:
_LOGGER.error("A connection has not been made to the ISY controller")
return False
devices = []
devices_by_nid = {}
child_nodes = []
for node in isy.filter_nodes(isy.SENSOR_NODES, units=UOM,
states=STATES):
for node in hass.data[ISY994_NODES][DOMAIN]:
if node.parent_node is None:
device = ISYBinarySensorDevice(node)
devices.append(device)
@@ -62,10 +55,9 @@ def setup_platform(hass, config: ConfigType,
node.nid, node.parent_nid)
else:
device_type = _detect_device_type(node)
if device_type in ['moisture', 'opening']:
subnode_id = int(node.nid[-1])
# Leak and door/window sensors work the same way with negative
# nodes and heartbeat nodes
subnode_id = int(node.nid[-1])
if device_type == 'opening':
# Door/window sensors use an optional "negative" node
if subnode_id == 4:
# Subnode 4 is the heartbeat node, which we will represent
# as a separate binary_sensor
@@ -74,19 +66,22 @@ def setup_platform(hass, config: ConfigType,
devices.append(device)
elif subnode_id == 2:
parent_device.add_negative_node(node)
elif device_type == 'moisture':
# Moisure nodes have a subnode 2, but we ignore it because it's
# just the inverse of the primary node.
if subnode_id == 4:
# Heartbeat node
device = ISYBinarySensorHeartbeat(node, parent_device)
parent_device.add_heartbeat_device(device)
devices.append(device)
else:
# We don't yet have any special logic for other sensor types,
# so add the nodes as individual devices
device = ISYBinarySensorDevice(node)
devices.append(device)
for program in isy.PROGRAMS.get(DOMAIN, []):
try:
status = program[isy.KEY_STATUS]
except (KeyError, AssertionError):
pass
else:
devices.append(ISYBinarySensorProgram(program.name, status))
for name, status, _ in hass.data[ISY994_PROGRAMS][DOMAIN]:
devices.append(ISYBinarySensorProgram(name, status))
add_devices(devices)
@@ -111,7 +106,7 @@ def _is_val_unknown(val):
return val == -1*float('inf')
class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice):
class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
"""Representation of an ISY994 binary sensor device.
Often times, a single device is represented by multiple nodes in the ISY,
@@ -165,7 +160,8 @@ class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice):
"""
self._negative_node = child
if not _is_val_unknown(self._negative_node):
# pylint: disable=protected-access
if not _is_val_unknown(self._negative_node.status._val):
# If the negative node has a value, it means the negative node is
# in use for this device. Therefore, we cannot determine the state
# of the sensor until we receive our first ON event.
@@ -250,7 +246,7 @@ class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice):
return self._device_class_from_type
class ISYBinarySensorHeartbeat(isy.ISYDevice, BinarySensorDevice):
class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice):
"""Representation of the battery state of an ISY994 sensor."""
def __init__(self, node, parent_device) -> None:
@@ -353,7 +349,7 @@ class ISYBinarySensorHeartbeat(isy.ISYDevice, BinarySensorDevice):
return attr
class ISYBinarySensorProgram(isy.ISYDevice, BinarySensorDevice):
class ISYBinarySensorProgram(ISYDevice, BinarySensorDevice):
"""Representation of an ISY994 binary sensor program.
This does not need all of the subnode logic in the device version of binary
@@ -129,6 +129,11 @@ class KNXBinarySensor(BinarySensorDevice):
"""Return the name of the KNX device."""
return self.device.name
@property
def available(self):
"""Return True if entity is available."""
return self.hass.data[DATA_KNX].connected
@property
def should_poll(self):
"""No polling needed within KNX."""
+9 -40
View File
@@ -17,19 +17,15 @@ from homeassistant.const import (
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF,
CONF_DEVICE_CLASS)
from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_QOS, valid_subscribe_topic)
CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_PAYLOAD_AVAILABLE = 'payload_available'
CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available'
DEFAULT_NAME = 'MQTT Binary sensor'
DEFAULT_PAYLOAD_OFF = 'OFF'
DEFAULT_PAYLOAD_ON = 'ON'
DEFAULT_PAYLOAD_AVAILABLE = 'online'
DEFAULT_PAYLOAD_NOT_AVAILABLE = 'offline'
DEPENDENCIES = ['mqtt']
@@ -38,12 +34,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_AVAILABILITY_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_PAYLOAD_AVAILABLE,
default=DEFAULT_PAYLOAD_AVAILABLE): cv.string,
vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE,
default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string,
})
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine
@@ -70,31 +61,29 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
)])
class MqttBinarySensor(BinarySensorDevice):
class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
"""Representation a binary sensor that is updated by MQTT."""
def __init__(self, name, state_topic, availability_topic, device_class,
qos, payload_on, payload_off, payload_available,
payload_not_available, value_template):
"""Initialize the MQTT binary sensor."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
self._name = name
self._state = None
self._state_topic = state_topic
self._availability_topic = availability_topic
self._available = True if availability_topic is None else False
self._device_class = device_class
self._payload_on = payload_on
self._payload_off = payload_off
self._payload_available = payload_available
self._payload_not_available = payload_not_available
self._qos = qos
self._template = value_template
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe mqtt events.
"""Subscribe mqtt events."""
yield from super().async_added_to_hass()
This method must be run in the event loop and returns a coroutine.
"""
@callback
def state_message_received(topic, payload, qos):
"""Handle a new received MQTT state message."""
@@ -111,21 +100,6 @@ class MqttBinarySensor(BinarySensorDevice):
yield from mqtt.async_subscribe(
self.hass, self._state_topic, state_message_received, self._qos)
@callback
def availability_message_received(topic, payload, qos):
"""Handle a new received MQTT availability message."""
if payload == self._payload_available:
self._available = True
elif payload == self._payload_not_available:
self._available = False
self.async_schedule_update_ha_state()
if self._availability_topic is not None:
yield from mqtt.async_subscribe(
self.hass, self._availability_topic,
availability_message_received, self._qos)
@property
def should_poll(self):
"""Return the polling state."""
@@ -136,11 +110,6 @@ class MqttBinarySensor(BinarySensorDevice):
"""Return the name of the binary sensor."""
return self._name
@property
def available(self) -> bool:
"""Return if the binary sensor is available."""
return self._available
@property
def is_on(self):
"""Return true if the binary sensor is on."""
@@ -98,6 +98,11 @@ class RestBinarySensor(BinarySensorDevice):
"""Return the class of this sensor."""
return self._device_class
@property
def available(self):
"""Return the availability of this sensor."""
return self.rest.data is not None
@property
def is_on(self):
"""Return true if the binary sensor is on."""
@@ -7,30 +7,40 @@ tested. Other types may need some work.
"""
import logging
import voluptuous as vol
from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF, CONF_NAME)
from homeassistant.components import rfxtrx
from homeassistant.helpers import event as evt
from homeassistant.helpers import config_validation as cv
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.rfxtrx import (
ATTR_NAME, ATTR_DATA_BITS, ATTR_OFF_DELAY, ATTR_FIRE_EVENT,
CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT,
CONF_DATA_BITS, CONF_DEVICES)
from homeassistant.util import slugify
from homeassistant.util import dt as dt_util
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import event as evt
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.rfxtrx import (
ATTR_AUTOMATIC_ADD, ATTR_NAME, ATTR_OFF_DELAY, ATTR_FIREEVENT,
ATTR_DATA_BITS, CONF_DEVICES
)
from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF
)
DEPENDENCIES = ["rfxtrx"]
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = vol.Schema({
vol.Required("platform"): rfxtrx.DOMAIN,
vol.Optional(CONF_DEVICES, default={}): vol.All(
dict, rfxtrx.valid_binary_sensor),
vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean,
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DEVICES, default={}): {
cv.string: vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): cv.string,
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
vol.Optional(CONF_DATA_BITS): cv.positive_int,
vol.Optional(CONF_COMMAND_ON): cv.byte,
vol.Optional(CONF_COMMAND_OFF): cv.byte
})
},
vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean,
}, extra=vol.ALLOW_EXTRA)
@@ -46,17 +56,17 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
if device_id in rfxtrx.RFX_DEVICES:
continue
if entity[ATTR_DATA_BITS] is not None:
_LOGGER.info("Masked device id: %s",
rfxtrx.get_pt2262_deviceid(device_id,
entity[ATTR_DATA_BITS]))
if entity[CONF_DATA_BITS] is not None:
_LOGGER.debug("Masked device id: %s",
rfxtrx.get_pt2262_deviceid(device_id,
entity[ATTR_DATA_BITS]))
_LOGGER.info("Add %s rfxtrx.binary_sensor (class %s)",
entity[ATTR_NAME], entity[CONF_DEVICE_CLASS])
_LOGGER.debug("Add %s rfxtrx.binary_sensor (class %s)",
entity[ATTR_NAME], entity[CONF_DEVICE_CLASS])
device = RfxtrxBinarySensor(event, entity[ATTR_NAME],
entity[CONF_DEVICE_CLASS],
entity[ATTR_FIREEVENT],
entity[ATTR_FIRE_EVENT],
entity[ATTR_OFF_DELAY],
entity[ATTR_DATA_BITS],
entity[CONF_COMMAND_ON],
@@ -82,15 +92,15 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
if sensor is None:
# Add the entity if not exists and automatic_add is True
if not config[ATTR_AUTOMATIC_ADD]:
if not config[CONF_AUTOMATIC_ADD]:
return
if event.device.packettype == 0x13:
poss_dev = rfxtrx.find_possible_pt2262_device(device_id)
if poss_dev is not None:
poss_id = slugify(poss_dev.event.device.id_string.lower())
_LOGGER.info("Found possible matching deviceid %s.",
poss_id)
_LOGGER.debug("Found possible matching deviceid %s.",
poss_id)
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
sensor = RfxtrxBinarySensor(event, pkt_id)
@@ -107,11 +117,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
elif not isinstance(sensor, RfxtrxBinarySensor):
return
else:
_LOGGER.info("Binary sensor update "
"(Device_id: %s Class: %s Sub: %s)",
slugify(event.device.id_string.lower()),
event.device.__class__.__name__,
event.device.subtype)
_LOGGER.debug("Binary sensor update "
"(Device_id: %s Class: %s Sub: %s)",
slugify(event.device.id_string.lower()),
event.device.__class__.__name__,
event.device.subtype)
if sensor.is_lighting4:
if sensor.data_bits is not None:
@@ -163,10 +173,8 @@ class RfxtrxBinarySensor(BinarySensorDevice):
self._masked_id = rfxtrx.get_pt2262_deviceid(
event.device.id_string.lower(),
data_bits)
def __str__(self):
"""Return the name of the sensor."""
return self._name
else:
self._masked_id = None
@property
def name(self):
@@ -38,6 +38,11 @@ SENSOR_SCHEMA = vol.Schema({
vol.All(cv.time_period, cv.positive_timedelta),
})
SENSOR_SCHEMA = vol.All(
cv.deprecated(ATTR_ENTITY_ID),
SENSOR_SCHEMA,
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}),
})
@@ -9,40 +9,48 @@ import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
from homeassistant.const import (
CONF_NAME, CONF_ENTITY_ID, CONF_TYPE, STATE_UNKNOWN,
ATTR_ENTITY_ID, CONF_DEVICE_CLASS)
ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_NAME,
STATE_UNKNOWN)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_state_change
_LOGGER = logging.getLogger(__name__)
ATTR_HYSTERESIS = 'hysteresis'
ATTR_LOWER = 'lower'
ATTR_POSITION = 'position'
ATTR_SENSOR_VALUE = 'sensor_value'
ATTR_THRESHOLD = 'threshold'
ATTR_TYPE = 'type'
ATTR_UPPER = 'upper'
CONF_HYSTERESIS = 'hysteresis'
CONF_LOWER = 'lower'
CONF_THRESHOLD = 'threshold'
CONF_UPPER = 'upper'
DEFAULT_NAME = 'Threshold'
DEFAULT_HYSTERESIS = 0.0
SENSOR_TYPES = [CONF_LOWER, CONF_UPPER]
POSITION_ABOVE = 'above'
POSITION_BELOW = 'below'
POSITION_IN_RANGE = 'in_range'
POSITION_UNKNOWN = 'unknown'
TYPE_LOWER = 'lower'
TYPE_RANGE = 'range'
TYPE_UPPER = 'upper'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_THRESHOLD): vol.Coerce(float),
vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES),
vol.Optional(
CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS): vol.Coerce(float),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS):
vol.Coerce(float),
vol.Optional(CONF_LOWER): vol.Coerce(float),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UPPER): vol.Coerce(float),
})
@@ -51,47 +59,44 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Threshold sensor."""
entity_id = config.get(CONF_ENTITY_ID)
name = config.get(CONF_NAME)
threshold = config.get(CONF_THRESHOLD)
lower = config.get(CONF_LOWER)
upper = config.get(CONF_UPPER)
hysteresis = config.get(CONF_HYSTERESIS)
limit_type = config.get(CONF_TYPE)
device_class = config.get(CONF_DEVICE_CLASS)
async_add_devices([ThresholdSensor(
hass, entity_id, name, threshold,
hysteresis, limit_type, device_class)
], True)
return True
hass, entity_id, name, lower, upper, hysteresis, device_class)], True)
class ThresholdSensor(BinarySensorDevice):
"""Representation of a Threshold sensor."""
def __init__(self, hass, entity_id, name, threshold,
hysteresis, limit_type, device_class):
def __init__(self, hass, entity_id, name, lower, upper, hysteresis,
device_class):
"""Initialize the Threshold sensor."""
self._hass = hass
self._entity_id = entity_id
self.is_upper = limit_type == 'upper'
self._name = name
self._threshold = threshold
self._threshold_lower = lower
self._threshold_upper = upper
self._hysteresis = hysteresis
self._device_class = device_class
self._state = False
self.sensor_value = 0
@callback
self._state_position = None
self._state = False
self.sensor_value = None
# pylint: disable=invalid-name
@callback
def async_threshold_sensor_state_listener(
entity, old_state, new_state):
"""Handle sensor state changes."""
if new_state.state == STATE_UNKNOWN:
return
try:
self.sensor_value = float(new_state.state)
except ValueError:
_LOGGER.error("State is not numerical")
self.sensor_value = None if new_state.state == STATE_UNKNOWN \
else float(new_state.state)
except (ValueError, TypeError):
self.sensor_value = None
_LOGGER.warning("State is not numerical")
hass.async_add_job(self.async_update_ha_state, True)
@@ -118,23 +123,67 @@ class ThresholdSensor(BinarySensorDevice):
"""Return the sensor class of the sensor."""
return self._device_class
@property
def threshold_type(self):
"""Return the type of threshold this sensor represents."""
if self._threshold_lower and self._threshold_upper:
return TYPE_RANGE
elif self._threshold_lower:
return TYPE_LOWER
elif self._threshold_upper:
return TYPE_UPPER
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
return {
ATTR_ENTITY_ID: self._entity_id,
ATTR_SENSOR_VALUE: self.sensor_value,
ATTR_THRESHOLD: self._threshold,
ATTR_HYSTERESIS: self._hysteresis,
ATTR_TYPE: CONF_UPPER if self.is_upper else CONF_LOWER,
ATTR_LOWER: self._threshold_lower,
ATTR_POSITION: self._state_position,
ATTR_SENSOR_VALUE: self.sensor_value,
ATTR_TYPE: self.threshold_type,
ATTR_UPPER: self._threshold_upper,
}
@asyncio.coroutine
def async_update(self):
"""Get the latest data and updates the states."""
if self._hysteresis == 0 and self.sensor_value == self._threshold:
def below(threshold):
"""Determine if the sensor value is below a threshold."""
return self.sensor_value < (threshold - self._hysteresis)
def above(threshold):
"""Determine if the sensor value is above a threshold."""
return self.sensor_value > (threshold + self._hysteresis)
if self.sensor_value is None:
self._state_position = POSITION_UNKNOWN
self._state = False
elif self.sensor_value > (self._threshold + self._hysteresis):
self._state = self.is_upper
elif self.sensor_value < (self._threshold - self._hysteresis):
self._state = not self.is_upper
elif self.threshold_type == TYPE_LOWER:
if below(self._threshold_lower):
self._state_position = POSITION_BELOW
self._state = True
elif above(self._threshold_lower):
self._state_position = POSITION_ABOVE
self._state = False
elif self.threshold_type == TYPE_UPPER:
if above(self._threshold_upper):
self._state_position = POSITION_ABOVE
self._state = True
elif below(self._threshold_upper):
self._state_position = POSITION_BELOW
self._state = False
elif self.threshold_type == TYPE_RANGE:
if below(self._threshold_lower):
self._state_position = POSITION_BELOW
self._state = False
if above(self._threshold_upper):
self._state_position = POSITION_ABOVE
self._state = False
elif above(self._threshold_lower) and below(self._threshold_upper):
self._state_position = POSITION_IN_RANGE
self._state = True
+12 -14
View File
@@ -11,21 +11,19 @@ import math
import voluptuous as vol
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, PLATFORM_SCHEMA,
BinarySensorDevice)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, CONF_DEVICE_CLASS, CONF_ENTITY_ID,
CONF_FRIENDLY_NAME, STATE_UNKNOWN)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA,
DEVICE_CLASSES_SCHEMA)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME,
CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FRIENDLY_NAME,
STATE_UNKNOWN)
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.event import async_track_state_change
from homeassistant.util import utcnow
REQUIREMENTS = ['numpy==1.13.3']
REQUIREMENTS = ['numpy==1.14.0']
_LOGGER = logging.getLogger(__name__)
@@ -36,21 +34,21 @@ ATTR_INVERT = 'invert'
ATTR_SAMPLE_DURATION = 'sample_duration'
ATTR_SAMPLE_COUNT = 'sample_count'
CONF_SENSORS = 'sensors'
CONF_ATTRIBUTE = 'attribute'
CONF_INVERT = 'invert'
CONF_MAX_SAMPLES = 'max_samples'
CONF_MIN_GRADIENT = 'min_gradient'
CONF_INVERT = 'invert'
CONF_SAMPLE_DURATION = 'sample_duration'
CONF_SENSORS = 'sensors'
SENSOR_SCHEMA = vol.Schema({
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_ATTRIBUTE): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_FRIENDLY_NAME): cv.string,
vol.Optional(CONF_INVERT, default=False): cv.boolean,
vol.Optional(CONF_MAX_SAMPLES, default=2): cv.positive_int,
vol.Optional(CONF_MIN_GRADIENT, default=0.0): vol.Coerce(float),
vol.Optional(CONF_INVERT, default=False): cv.boolean,
vol.Optional(CONF_SAMPLE_DURATION, default=0): cv.positive_int,
})
@@ -129,11 +127,11 @@ class SensorTrend(BinarySensorDevice):
return {
ATTR_ENTITY_ID: self._entity_id,
ATTR_FRIENDLY_NAME: self._name,
ATTR_INVERT: self._invert,
ATTR_GRADIENT: self._gradient,
ATTR_INVERT: self._invert,
ATTR_MIN_GRADIENT: self._min_gradient,
ATTR_SAMPLE_DURATION: self._sample_duration,
ATTR_SAMPLE_COUNT: len(self.samples),
ATTR_SAMPLE_DURATION: self._sample_duration,
}
@property
@@ -64,7 +64,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
excludes = config.get(CONF_EXCLUDES)
days_offset = config.get(CONF_OFFSET)
year = (datetime.now() + timedelta(days=days_offset)).year
year = (get_date(datetime.today()) + timedelta(days=days_offset)).year
obj_holidays = getattr(holidays, country)(years=year)
if province:
@@ -99,6 +99,11 @@ def day_to_string(day):
return None
def get_date(date):
"""Return date. Needed for testing."""
return date
class IsWorkdaySensor(BinarySensorDevice):
"""Implementation of a Workday sensor."""
@@ -156,7 +161,7 @@ class IsWorkdaySensor(BinarySensorDevice):
self._state = False
# Get iso day of the week (1 = Monday, 7 = Sunday)
date = datetime.today() + timedelta(days=self._days_offset)
date = get_date(datetime.today()) + timedelta(days=self._days_offset)
day = date.isoweekday() - 1
day_of_week = day_to_string(day)
+3 -3
View File
@@ -25,11 +25,11 @@ CONF_DEVICE_ID = 'device_id'
CONF_CALENDARS = 'calendars'
CONF_CUSTOM_CALENDARS = 'custom_calendars'
CONF_CALENDAR = 'calendar'
CONF_ALL_DAY = 'all_day'
CONF_SEARCH = 'search'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_URL): vol.Url,
# pylint: disable=no-value-for-parameter
vol.Required(CONF_URL): vol.Url(),
vol.Optional(CONF_CALENDARS, default=[]):
vol.All(cv.ensure_list, vol.Schema([
cv.string
@@ -88,7 +88,7 @@ def setup_platform(hass, config, add_devices, disc_info=None):
WebDavCalendarEventDevice(hass,
device_data,
calendar,
cust_calendar.get(CONF_ALL_DAY),
True,
cust_calendar.get(CONF_SEARCH))
)
View File
@@ -9,7 +9,6 @@ https://home-assistant.io/components/calendar.todoist/
from datetime import datetime
from datetime import timedelta
import logging
import os
import voluptuous as vol
@@ -17,7 +16,6 @@ from homeassistant.components.calendar import (
CalendarEventDevice, PLATFORM_SCHEMA)
from homeassistant.components.google import (
CONF_DEVICE_ID)
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
CONF_ID, CONF_NAME, CONF_TOKEN)
import homeassistant.helpers.config_validation as cv
@@ -178,10 +176,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(project_devices)
# Services:
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
def handle_new_task(call):
"""Called when a user creates a new Todoist Task from HASS."""
project_name = call.data[PROJECT_NAME]
@@ -215,7 +209,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.debug("Created Todoist task: %s", call.data[CONTENT])
hass.services.register(DOMAIN, SERVICE_NEW_TASK, handle_new_task,
descriptions[DOMAIN][SERVICE_NEW_TASK],
schema=NEW_TASK_SERVICE_SCHEMA)
+2 -9
View File
@@ -12,7 +12,6 @@ from datetime import timedelta
import logging
import hashlib
from random import SystemRandom
import os
import aiohttp
from aiohttp import web
@@ -21,7 +20,6 @@ import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE)
from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import bind_hass
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -190,19 +188,14 @@ def async_setup(hass, config):
except OSError as err:
_LOGGER.error("Can't write image to file: %s", err)
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
hass.services.async_register(
DOMAIN, SERVICE_ENABLE_MOTION, async_handle_camera_service,
descriptions.get(SERVICE_ENABLE_MOTION), schema=CAMERA_SERVICE_SCHEMA)
schema=CAMERA_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_DISABLE_MOTION, async_handle_camera_service,
descriptions.get(SERVICE_DISABLE_MOTION), schema=CAMERA_SERVICE_SCHEMA)
schema=CAMERA_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_SNAPSHOT, async_handle_snapshot_service,
descriptions.get(SERVICE_SNAPSHOT),
schema=CAMERA_SERVICE_SNAPSHOT)
return True
+14 -26
View File
@@ -1,51 +1,40 @@
"""Support for viewing the camera feed from a DoorBird video doorbell."""
"""
Support for viewing the camera feed from a DoorBird video doorbell.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.doorbird/
"""
import asyncio
import datetime
import logging
import voluptuous as vol
import aiohttp
import async_timeout
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.components.camera import Camera
from homeassistant.components.doorbird import DOMAIN as DOORBIRD_DOMAIN
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
DEPENDENCIES = ['doorbird']
_CAMERA_LIVE = "DoorBird Live"
_CAMERA_LAST_VISITOR = "DoorBird Last Ring"
_LIVE_INTERVAL = datetime.timedelta(seconds=1)
_CAMERA_LIVE = "DoorBird Live"
_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=1)
_LIVE_INTERVAL = datetime.timedelta(seconds=1)
_LOGGER = logging.getLogger(__name__)
_TIMEOUT = 10 # seconds
CONF_SHOW_LAST_VISITOR = 'last_visitor'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SHOW_LAST_VISITOR, default=False): cv.boolean
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the DoorBird camera platform."""
device = hass.data.get(DOORBIRD_DOMAIN)
_LOGGER.debug("Adding DoorBird camera %s", _CAMERA_LIVE)
entities = [DoorBirdCamera(device.live_image_url, _CAMERA_LIVE,
_LIVE_INTERVAL)]
if config.get(CONF_SHOW_LAST_VISITOR):
_LOGGER.debug("Adding DoorBird camera %s", _CAMERA_LAST_VISITOR)
entities.append(DoorBirdCamera(device.history_image_url(1),
_CAMERA_LAST_VISITOR,
_LAST_VISITOR_INTERVAL))
async_add_devices(entities)
_LOGGER.info("Added DoorBird camera(s)")
async_add_devices([
DoorBirdCamera(device.live_image_url, _CAMERA_LIVE, _LIVE_INTERVAL),
DoorBirdCamera(
device.history_image_url(1, 'doorbell'), _CAMERA_LAST_VISITOR,
_LAST_VISITOR_INTERVAL),
])
class DoorBirdCamera(Camera):
@@ -75,7 +64,6 @@ class DoorBirdCamera(Camera):
try:
websession = async_get_clientsession(self.hass)
with async_timeout.timeout(_TIMEOUT, loop=self.hass.loop):
response = yield from websession.get(self._url)
+2 -4
View File
@@ -60,11 +60,9 @@ class MqttCamera(Camera):
"""Return the name of this camera."""
return self._name
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe MQTT events.
This method must be run in the event loop and returns a coroutine.
"""
"""Subscribe MQTT events."""
@callback
def message_received(topic, payload, qos):
"""Handle new MQTT messages."""
+30
View File
@@ -82,6 +82,7 @@ class UnifiVideoCamera(Camera):
self.is_streaming = False
self._connect_addr = None
self._camera = None
self._motion_status = False
@property
def name(self):
@@ -94,6 +95,12 @@ class UnifiVideoCamera(Camera):
caminfo = self._nvr.get_camera(self._uuid)
return caminfo['recordingSettings']['fullTimeRecordEnabled']
@property
def motion_detection_enabled(self):
"""Camera Motion Detection Status."""
caminfo = self._nvr.get_camera(self._uuid)
return caminfo['recordingSettings']['motionRecordEnabled']
@property
def brand(self):
"""Return the brand of this camera."""
@@ -165,3 +172,26 @@ class UnifiVideoCamera(Camera):
raise
return _get_image()
def set_motion_detection(self, mode):
"""Set motion detection on or off."""
from uvcclient.nvr import NvrError
if mode is True:
set_mode = 'motion'
else:
set_mode = 'none'
try:
self._nvr.set_recordmode(self._uuid, set_mode)
self._motion_status = mode
except NvrError as err:
_LOGGER.error("Unable to set recordmode to " + set_mode)
_LOGGER.debug(err)
def enable_motion_detection(self):
"""Enable motion detection in camera."""
self.set_motion_detection(True)
def disable_motion_detection(self):
"""Disable motion detection in camera."""
self.set_motion_detection(False)
+65 -17
View File
@@ -7,12 +7,10 @@ https://home-assistant.io/components/climate/
import asyncio
from datetime import timedelta
import logging
import os
import functools as ft
import voluptuous as vol
from homeassistant.config import load_yaml_config_file
from homeassistant.loader import bind_hass
from homeassistant.helpers.temperature import display_temp as show_temp
from homeassistant.util.temperature import convert as convert_temperature
@@ -21,9 +19,9 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
TEMP_CELSIUS, PRECISION_WHOLE, PRECISION_TENTHS)
ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF,
STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE,
PRECISION_TENTHS, )
DOMAIN = 'climate'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
@@ -63,6 +61,7 @@ SUPPORT_HOLD_MODE = 256
SUPPORT_SWING_MODE = 512
SUPPORT_AWAY_MODE = 1024
SUPPORT_AUX_HEAT = 2048
SUPPORT_ON_OFF = 4096
ATTR_CURRENT_TEMPERATURE = 'current_temperature'
ATTR_MAX_TEMP = 'max_temp'
@@ -92,6 +91,10 @@ CONVERTIBLE_ATTRIBUTE = [
_LOGGER = logging.getLogger(__name__)
ON_OFF_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
SET_AWAY_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_AWAY_MODE): cv.boolean,
@@ -240,10 +243,6 @@ def async_setup(hass, config):
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
yield from component.async_setup(config)
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine
def async_away_mode_set_service(service):
"""Set away mode on target climate devices."""
@@ -267,7 +266,6 @@ def async_setup(hass, config):
hass.services.async_register(
DOMAIN, SERVICE_SET_AWAY_MODE, async_away_mode_set_service,
descriptions.get(SERVICE_SET_AWAY_MODE),
schema=SET_AWAY_MODE_SCHEMA)
@asyncio.coroutine
@@ -290,7 +288,6 @@ def async_setup(hass, config):
hass.services.async_register(
DOMAIN, SERVICE_SET_HOLD_MODE, async_hold_mode_set_service,
descriptions.get(SERVICE_SET_HOLD_MODE),
schema=SET_HOLD_MODE_SCHEMA)
@asyncio.coroutine
@@ -316,7 +313,6 @@ def async_setup(hass, config):
hass.services.async_register(
DOMAIN, SERVICE_SET_AUX_HEAT, async_aux_heat_set_service,
descriptions.get(SERVICE_SET_AUX_HEAT),
schema=SET_AUX_HEAT_SCHEMA)
@asyncio.coroutine
@@ -348,7 +344,6 @@ def async_setup(hass, config):
hass.services.async_register(
DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service,
descriptions.get(SERVICE_SET_TEMPERATURE),
schema=SET_TEMPERATURE_SCHEMA)
@asyncio.coroutine
@@ -370,7 +365,6 @@ def async_setup(hass, config):
hass.services.async_register(
DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service,
descriptions.get(SERVICE_SET_HUMIDITY),
schema=SET_HUMIDITY_SCHEMA)
@asyncio.coroutine
@@ -392,7 +386,6 @@ def async_setup(hass, config):
hass.services.async_register(
DOMAIN, SERVICE_SET_FAN_MODE, async_fan_mode_set_service,
descriptions.get(SERVICE_SET_FAN_MODE),
schema=SET_FAN_MODE_SCHEMA)
@asyncio.coroutine
@@ -414,7 +407,6 @@ def async_setup(hass, config):
hass.services.async_register(
DOMAIN, SERVICE_SET_OPERATION_MODE, async_operation_set_service,
descriptions.get(SERVICE_SET_OPERATION_MODE),
schema=SET_OPERATION_MODE_SCHEMA)
@asyncio.coroutine
@@ -436,9 +428,34 @@ def async_setup(hass, config):
hass.services.async_register(
DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service,
descriptions.get(SERVICE_SET_SWING_MODE),
schema=SET_SWING_MODE_SCHEMA)
@asyncio.coroutine
def async_on_off_service(service):
"""Handle on/off calls."""
target_climate = component.async_extract_from_service(service)
update_tasks = []
for climate in target_climate:
if service.service == SERVICE_TURN_ON:
yield from climate.async_turn_on()
elif service.service == SERVICE_TURN_OFF:
yield from climate.async_turn_off()
if not climate.should_poll:
continue
update_tasks.append(climate.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_TURN_OFF, async_on_off_service,
schema=ON_OFF_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_TURN_ON, async_on_off_service,
schema=ON_OFF_SERVICE_SCHEMA)
return True
@@ -449,8 +466,12 @@ class ClimateDevice(Entity):
@property
def state(self):
"""Return the current state."""
if self.is_on is False:
return STATE_OFF
if self.current_operation:
return self.current_operation
if self.is_on:
return STATE_ON
return STATE_UNKNOWN
@property
@@ -594,6 +615,11 @@ class ClimateDevice(Entity):
"""Return the current hold mode, e.g., home, away, temp."""
return None
@property
def is_on(self):
"""Return true if on."""
return None
@property
def is_aux_heat_on(self):
"""Return true if aux heater."""
@@ -730,6 +756,28 @@ class ClimateDevice(Entity):
"""
return self.hass.async_add_job(self.turn_aux_heat_off)
def turn_on(self):
"""Turn device on."""
raise NotImplementedError()
def async_turn_on(self):
"""Turn device on.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.turn_on)
def turn_off(self):
"""Turn device off."""
raise NotImplementedError()
def async_turn_off(self):
"""Turn device off.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.turn_off)
@property
def supported_features(self):
"""Return the list of supported features."""
+257
View File
@@ -0,0 +1,257 @@
"""
Support for the Daikin HVAC.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.daikin/
"""
import logging
import re
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.climate import (
ATTR_OPERATION_MODE, ATTR_FAN_MODE, ATTR_SWING_MODE,
ATTR_CURRENT_TEMPERATURE, ClimateDevice, PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_SWING_MODE, STATE_OFF, STATE_AUTO, STATE_HEAT, STATE_COOL,
STATE_DRY, STATE_FAN_ONLY
)
from homeassistant.components.daikin import (
daikin_api_setup,
ATTR_TARGET_TEMPERATURE,
ATTR_INSIDE_TEMPERATURE,
ATTR_OUTSIDE_TEMPERATURE
)
from homeassistant.const import (
CONF_HOST, CONF_NAME,
TEMP_CELSIUS,
ATTR_TEMPERATURE
)
REQUIREMENTS = ['pydaikin==0.4']
_LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
SUPPORT_FAN_MODE |
SUPPORT_OPERATION_MODE |
SUPPORT_SWING_MODE)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=None): cv.string,
})
HA_STATE_TO_DAIKIN = {
STATE_FAN_ONLY: 'fan',
STATE_DRY: 'dry',
STATE_COOL: 'cool',
STATE_HEAT: 'hot',
STATE_AUTO: 'auto',
STATE_OFF: 'off',
}
HA_ATTR_TO_DAIKIN = {
ATTR_OPERATION_MODE: 'mode',
ATTR_FAN_MODE: 'f_rate',
ATTR_SWING_MODE: 'f_dir',
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Daikin HVAC platform."""
if discovery_info is not None:
host = discovery_info.get('ip')
name = None
_LOGGER.info("Discovered a Daikin AC on %s", host)
else:
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
_LOGGER.info("Added Daikin AC on %s", host)
api = daikin_api_setup(hass, host, name)
add_devices([DaikinClimate(api)], True)
class DaikinClimate(ClimateDevice):
"""Representation of a Daikin HVAC."""
def __init__(self, api):
"""Initialize the climate device."""
from pydaikin import appliance
self._api = api
self._force_refresh = False
self._list = {
ATTR_OPERATION_MODE: list(
map(str.title, set(HA_STATE_TO_DAIKIN.values()))
),
ATTR_FAN_MODE: list(
map(
str.title,
appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE])
)
),
ATTR_SWING_MODE: list(
map(
str.title,
appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_SWING_MODE])
)
),
}
def get(self, key):
"""Retrieve device settings from API library cache."""
value = None
cast_to_float = False
if key in [ATTR_TEMPERATURE, ATTR_INSIDE_TEMPERATURE,
ATTR_CURRENT_TEMPERATURE]:
value = self._api.device.values.get('htemp')
cast_to_float = True
if key == ATTR_TARGET_TEMPERATURE:
value = self._api.device.values.get('stemp')
cast_to_float = True
elif key == ATTR_OUTSIDE_TEMPERATURE:
value = self._api.device.values.get('otemp')
cast_to_float = True
elif key == ATTR_FAN_MODE:
value = self._api.device.represent('f_rate')[1].title()
elif key == ATTR_SWING_MODE:
value = self._api.device.represent('f_dir')[1].title()
elif key == ATTR_OPERATION_MODE:
# Daikin can return also internal states auto-1 or auto-7
# and we need to translate them as AUTO
value = re.sub(
'[^a-z]',
'',
self._api.device.represent('mode')[1]
).title()
if value is None:
_LOGGER.warning("Invalid value requested for key %s", key)
else:
if value == "-" or value == "--":
value = None
elif cast_to_float:
try:
value = float(value)
except ValueError:
value = None
return value
def set(self, settings):
"""Set device settings using API."""
values = {}
for attr in [ATTR_TEMPERATURE, ATTR_FAN_MODE, ATTR_SWING_MODE,
ATTR_OPERATION_MODE]:
value = settings.get(attr)
if value is None:
continue
daikin_attr = HA_ATTR_TO_DAIKIN.get(attr)
if daikin_attr is not None:
if value.title() in self._list[attr]:
values[daikin_attr] = value.lower()
else:
_LOGGER.error("Invalid value %s for %s", attr, value)
# temperature
elif attr == ATTR_TEMPERATURE:
try:
values['stemp'] = str(int(value))
except ValueError:
_LOGGER.error("Invalid temperature %s", value)
if values:
self._force_refresh = True
self._api.device.set(values)
@property
def unique_id(self):
"""Return the ID of this AC."""
return "{}.{}".format(self.__class__, self._api.ip_address)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property
def name(self):
"""Return the name of the thermostat, if any."""
return self._api.name
@property
def temperature_unit(self):
"""Return the unit of measurement which this thermostat uses."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
return self.get(ATTR_CURRENT_TEMPERATURE)
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self.get(ATTR_TARGET_TEMPERATURE)
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
return 1
def set_temperature(self, **kwargs):
"""Set new target temperature."""
self.set(kwargs)
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return self.get(ATTR_OPERATION_MODE)
@property
def operation_list(self):
"""Return the list of available operation modes."""
return self._list.get(ATTR_OPERATION_MODE)
def set_operation_mode(self, operation_mode):
"""Set HVAC mode."""
self.set({ATTR_OPERATION_MODE: operation_mode})
@property
def current_fan_mode(self):
"""Return the fan setting."""
return self.get(ATTR_FAN_MODE)
def set_fan_mode(self, fan):
"""Set fan mode."""
self.set({ATTR_FAN_MODE: fan})
@property
def fan_list(self):
"""List of available fan modes."""
return self._list.get(ATTR_FAN_MODE)
@property
def current_swing_mode(self):
"""Return the fan setting."""
return self.get(ATTR_SWING_MODE)
def set_swing_mode(self, swing_mode):
"""Set new target temperature."""
self.set({ATTR_SWING_MODE: swing_mode})
@property
def swing_list(self):
"""List of available swing modes."""
return self._list.get(ATTR_SWING_MODE)
def update(self):
"""Retrieve latest state."""
self._api.update(no_throttle=self._force_refresh)
self._force_refresh = False
+19 -2
View File
@@ -9,14 +9,15 @@ from homeassistant.components.climate import (
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY,
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_FAN_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_AUX_HEAT, SUPPORT_SWING_MODE,
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
SUPPORT_ON_OFF)
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY |
SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_FAN_MODE |
SUPPORT_OPERATION_MODE | SUPPORT_AUX_HEAT |
SUPPORT_SWING_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_TARGET_TEMPERATURE_LOW)
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_ON_OFF)
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -56,6 +57,7 @@ class DemoClimate(ClimateDevice):
self._swing_list = ['Auto', '1', '2', '3', 'Off']
self._target_temperature_high = target_temp_high
self._target_temperature_low = target_temp_low
self._on = True
@property
def supported_features(self):
@@ -132,6 +134,11 @@ class DemoClimate(ClimateDevice):
"""Return true if aux heat is on."""
return self._aux
@property
def is_on(self):
"""Return true if the device is on."""
return self._on
@property
def current_fan_mode(self):
"""Return the fan setting."""
@@ -206,3 +213,13 @@ class DemoClimate(ClimateDevice):
"""Turn auxiliary heater off."""
self._aux = False
self.schedule_update_ha_state()
def turn_on(self):
"""Turn on."""
self._on = True
self.schedule_update_ha_state()
def turn_off(self):
"""Turn off."""
self._on = False
self.schedule_update_ha_state()
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.ecobee/
"""
import logging
from os import path
import voluptuous as vol
@@ -17,7 +16,6 @@ from homeassistant.components.climate import (
SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH)
from homeassistant.const import (
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
_CONFIGURING = {}
@@ -96,17 +94,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
thermostat.schedule_update_ha_state(True)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(
DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, fan_min_on_time_set_service,
descriptions.get(SERVICE_SET_FAN_MIN_ON_TIME),
schema=SET_FAN_MIN_ON_TIME_SCHEMA)
hass.services.register(
DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service,
descriptions.get(SERVICE_RESUME_PROGRAM),
schema=RESUME_PROGRAM_SCHEMA)
+228
View File
@@ -0,0 +1,228 @@
"""
Support for Rheem EcoNet water heaters.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.econet/
"""
import datetime
import logging
import voluptuous as vol
from homeassistant.components.climate import (
DOMAIN,
PLATFORM_SCHEMA,
STATE_ECO, STATE_GAS, STATE_ELECTRIC,
STATE_HEAT_PUMP, STATE_HIGH_DEMAND,
STATE_OFF, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE,
ClimateDevice)
from homeassistant.const import (ATTR_ENTITY_ID,
CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT,
ATTR_TEMPERATURE)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyeconet==0.0.4']
_LOGGER = logging.getLogger(__name__)
ATTR_VACATION_START = 'next_vacation_start_date'
ATTR_VACATION_END = 'next_vacation_end_date'
ATTR_ON_VACATION = 'on_vacation'
ATTR_TODAYS_ENERGY_USAGE = 'todays_energy_usage'
ATTR_IN_USE = 'in_use'
ATTR_START_DATE = 'start_date'
ATTR_END_DATE = 'end_date'
SUPPORT_FLAGS_HEATER = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE)
SERVICE_ADD_VACATION = 'econet_add_vacation'
SERVICE_DELETE_VACATION = 'econet_delete_vacation'
ADD_VACATION_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_START_DATE): cv.positive_int,
vol.Required(ATTR_END_DATE): cv.positive_int,
})
DELETE_VACATION_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
ECONET_DATA = 'econet'
HA_STATE_TO_ECONET = {
STATE_ECO: 'Energy Saver',
STATE_ELECTRIC: 'Electric',
STATE_HEAT_PUMP: 'Heat Pump',
STATE_GAS: 'gas',
STATE_HIGH_DEMAND: 'High Demand',
STATE_OFF: 'Off',
}
ECONET_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_ECONET.items()}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the EcoNet water heaters."""
from pyeconet.api import PyEcoNet
hass.data[ECONET_DATA] = {}
hass.data[ECONET_DATA]['water_heaters'] = []
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
econet = PyEcoNet(username, password)
water_heaters = econet.get_water_heaters()
hass_water_heaters = [
EcoNetWaterHeater(water_heater) for water_heater in water_heaters]
add_devices(hass_water_heaters)
hass.data[ECONET_DATA]['water_heaters'].extend(hass_water_heaters)
def service_handle(service):
"""Handler for services."""
entity_ids = service.data.get('entity_id')
all_heaters = hass.data[ECONET_DATA]['water_heaters']
_heaters = [
x for x in all_heaters
if not entity_ids or x.entity_id in entity_ids]
for _water_heater in _heaters:
if service.service == SERVICE_ADD_VACATION:
start = service.data.get(ATTR_START_DATE)
end = service.data.get(ATTR_END_DATE)
_water_heater.add_vacation(start, end)
if service.service == SERVICE_DELETE_VACATION:
for vacation in _water_heater.water_heater.vacations:
vacation.delete()
_water_heater.schedule_update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_ADD_VACATION,
service_handle,
schema=ADD_VACATION_SCHEMA)
hass.services.register(DOMAIN, SERVICE_DELETE_VACATION,
service_handle,
schema=DELETE_VACATION_SCHEMA)
class EcoNetWaterHeater(ClimateDevice):
"""Representation of an EcoNet water heater."""
def __init__(self, water_heater):
"""Initialize the water heater."""
self.water_heater = water_heater
@property
def name(self):
"""Return the device name."""
return self.water_heater.name
@property
def available(self):
"""Return if the the device is online or not."""
return self.water_heater.is_connected
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@property
def device_state_attributes(self):
"""Return the optional state attributes."""
data = {}
vacations = self.water_heater.get_vacations()
if vacations:
data[ATTR_VACATION_START] = vacations[0].start_date
data[ATTR_VACATION_END] = vacations[0].end_date
data[ATTR_ON_VACATION] = self.water_heater.is_on_vacation
todays_usage = self.water_heater.total_usage_for_today
if todays_usage:
data[ATTR_TODAYS_ENERGY_USAGE] = todays_usage
data[ATTR_IN_USE] = self.water_heater.in_use
return data
@property
def current_operation(self):
"""
Return current operation as one of the following.
["eco", "heat_pump",
"high_demand", "electric_only"]
"""
current_op = ECONET_STATE_TO_HA.get(self.water_heater.mode)
return current_op
@property
def operation_list(self):
"""List of available operation modes."""
op_list = []
modes = self.water_heater.supported_modes
for mode in modes:
ha_mode = ECONET_STATE_TO_HA.get(mode)
if ha_mode is not None:
op_list.append(ha_mode)
else:
error = "Invalid operation mode mapping. " + mode + \
" doesn't map. Please report this."
_LOGGER.error(error)
return op_list
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS_HEATER
def set_temperature(self, **kwargs):
"""Set new target temperature."""
target_temp = kwargs.get(ATTR_TEMPERATURE)
if target_temp is not None:
self.water_heater.set_target_set_point(target_temp)
else:
_LOGGER.error("A target temperature must be provided.")
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
op_mode_to_set = HA_STATE_TO_ECONET.get(operation_mode)
if op_mode_to_set is not None:
self.water_heater.set_mode(op_mode_to_set)
else:
_LOGGER.error("An operation mode must be provided.")
def add_vacation(self, start, end):
"""Add a vacation to this water heater."""
if not start:
start = datetime.datetime.now()
else:
start = datetime.datetime.fromtimestamp(start)
end = datetime.datetime.fromtimestamp(end)
self.water_heater.set_vacation_mode(start, end)
def update(self):
"""Get the latest date."""
self.water_heater.update_state()
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self.water_heater.set_point
@property
def min_temp(self):
"""Return the minimum temperature."""
return self.water_heater.min_set_point
@property
def max_temp(self):
"""Return the maximum temperature."""
return self.water_heater.max_set_point
@@ -12,9 +12,9 @@ import voluptuous as vol
from homeassistant.core import callback
from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
STATE_AUTO, ATTR_OPERATION_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_TEMPERATURE)
STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice,
ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA)
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
@@ -30,6 +30,7 @@ DEPENDENCIES = ['switch', 'sensor']
DEFAULT_TOLERANCE = 0.3
DEFAULT_NAME = 'Generic Thermostat'
DEFAULT_AWAY_TEMP = 16
CONF_HEATER = 'heater'
CONF_SENSOR = 'target_sensor'
@@ -42,7 +43,9 @@ CONF_COLD_TOLERANCE = 'cold_tolerance'
CONF_HOT_TOLERANCE = 'hot_tolerance'
CONF_KEEP_ALIVE = 'keep_alive'
CONF_INITIAL_OPERATION_MODE = 'initial_operation_mode'
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
CONF_AWAY_TEMP = 'away_temp'
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
SUPPORT_OPERATION_MODE)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HEATER): cv.entity_id,
@@ -60,7 +63,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_KEEP_ALIVE): vol.All(
cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_INITIAL_OPERATION_MODE):
vol.In([STATE_AUTO, STATE_OFF])
vol.In([STATE_AUTO, STATE_OFF]),
vol.Optional(CONF_AWAY_TEMP,
default=DEFAULT_AWAY_TEMP): vol.Coerce(float)
})
@@ -79,11 +84,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
hot_tolerance = config.get(CONF_HOT_TOLERANCE)
keep_alive = config.get(CONF_KEEP_ALIVE)
initial_operation_mode = config.get(CONF_INITIAL_OPERATION_MODE)
away_temp = config.get(CONF_AWAY_TEMP)
async_add_devices([GenericThermostat(
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
target_temp, ac_mode, min_cycle_duration, cold_tolerance,
hot_tolerance, keep_alive, initial_operation_mode)])
hot_tolerance, keep_alive, initial_operation_mode, away_temp)])
class GenericThermostat(ClimateDevice):
@@ -92,7 +98,7 @@ class GenericThermostat(ClimateDevice):
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
cold_tolerance, hot_tolerance, keep_alive,
initial_operation_mode):
initial_operation_mode, away_temp):
"""Initialize the thermostat."""
self.hass = hass
self._name = name
@@ -103,17 +109,26 @@ class GenericThermostat(ClimateDevice):
self._hot_tolerance = hot_tolerance
self._keep_alive = keep_alive
self._initial_operation_mode = initial_operation_mode
self._saved_target_temp = target_temp if target_temp is not None \
else away_temp
if self.ac_mode:
self._current_operation = STATE_COOL
self._operation_list = [STATE_COOL, STATE_OFF]
else:
self._current_operation = STATE_HEAT
self._operation_list = [STATE_HEAT, STATE_OFF]
if initial_operation_mode == STATE_OFF:
self._enabled = False
else:
self._enabled = True
self._active = False
self._cur_temp = None
self._min_temp = min_temp
self._max_temp = max_temp
self._target_temp = target_temp
self._unit = hass.config.units.temperature_unit
self._away_temp = away_temp
self._is_away = False
async_track_state_change(
hass, sensor_entity_id, self._async_sensor_changed)
@@ -124,10 +139,6 @@ class GenericThermostat(ClimateDevice):
async_track_time_interval(
hass, self._async_keep_alive, self._keep_alive)
sensor_state = hass.states.get(sensor_entity_id)
if sensor_state:
self._async_update_temp(sensor_state)
@asyncio.coroutine
def async_added_to_hass(self):
"""Run when entity about to be added."""
@@ -137,14 +148,37 @@ class GenericThermostat(ClimateDevice):
if old_state is not None:
# If we have no initial temperature, restore
if self._target_temp is None:
self._target_temp = float(
old_state.attributes[ATTR_TEMPERATURE])
# If we have no initial operation mode, restore
# If we have a previously saved temperature
if old_state.attributes[ATTR_TEMPERATURE] is None:
if self.ac_mode:
self._target_temp = self.max_temp
else:
self._target_temp = self.min_temp
_LOGGER.warning('Undefined target temperature, \
falling back to %s', self._target_temp)
else:
self._target_temp = float(
old_state.attributes[ATTR_TEMPERATURE])
self._is_away = True if str(
old_state.attributes[ATTR_AWAY_MODE]) == STATE_ON else False
if old_state.attributes[ATTR_OPERATION_MODE] == STATE_OFF:
self._current_operation = STATE_OFF
self._enabled = False
if self._initial_operation_mode is None:
if old_state.attributes[ATTR_OPERATION_MODE] == STATE_OFF:
self._enabled = False
@property
def state(self):
"""Return the current state."""
if self._is_device_active:
return self.current_operation
else:
if self._enabled:
return STATE_IDLE
else:
return STATE_OFF
@property
def should_poll(self):
"""Return the polling state."""
@@ -167,15 +201,8 @@ class GenericThermostat(ClimateDevice):
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
if not self._enabled:
return STATE_OFF
if self.ac_mode:
cooling = self._active and self._is_device_active
return STATE_COOL if cooling else STATE_IDLE
heating = self._active and self._is_device_active
return STATE_HEAT if heating else STATE_IDLE
"""Return current operation."""
return self._current_operation
@property
def target_temperature(self):
@@ -185,14 +212,20 @@ class GenericThermostat(ClimateDevice):
@property
def operation_list(self):
"""List of available operation modes."""
return [STATE_AUTO, STATE_OFF]
return self._operation_list
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
if operation_mode == STATE_AUTO:
if operation_mode == STATE_HEAT:
self._current_operation = STATE_HEAT
self._enabled = True
self._async_control_heating()
elif operation_mode == STATE_COOL:
self._current_operation = STATE_COOL
self._enabled = True
self._async_control_heating()
elif operation_mode == STATE_OFF:
self._current_operation = STATE_OFF
self._enabled = False
if self._is_device_active:
self._heater_turn_off()
@@ -252,7 +285,7 @@ class GenericThermostat(ClimateDevice):
@callback
def _async_keep_alive(self, time):
"""Call at constant intervals for keep-alive purposes."""
if self.current_operation in [STATE_COOL, STATE_HEAT]:
if self._is_device_active:
self._heater_turn_on()
else:
self._heater_turn_off()
@@ -347,3 +380,23 @@ class GenericThermostat(ClimateDevice):
data = {ATTR_ENTITY_ID: self.heater_entity_id}
self.hass.async_add_job(
self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_OFF, data))
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return self._is_away
def turn_away_mode_on(self):
"""Turn away mode on by setting it on away hold indefinitely."""
self._is_away = True
self._saved_target_temp = self._target_temp
self._target_temp = self._away_temp
self._async_control_heating()
self.schedule_update_ha_state()
def turn_away_mode_off(self):
"""Turn away off."""
self._is_away = False
self._target_temp = self._saved_target_temp
self._async_control_heating()
self.schedule_update_ha_state()
+41 -2
View File
@@ -6,7 +6,7 @@ https://home-assistant.io/components/climate.hive/
"""
from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.components.hive import DATA_HIVE
@@ -16,7 +16,9 @@ HIVE_TO_HASS_STATE = {'SCHEDULE': STATE_AUTO, 'MANUAL': STATE_HEAT,
HASS_TO_HIVE_STATE = {STATE_AUTO: 'SCHEDULE', STATE_HEAT: 'MANUAL',
STATE_ON: 'ON', STATE_OFF: 'OFF'}
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
SUPPORT_OPERATION_MODE |
SUPPORT_AUX_HEAT)
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -134,6 +136,43 @@ class HiveClimateEntity(ClimateDevice):
for entity in self.session.entities:
entity.handle_update(self.data_updatesource)
@property
def is_aux_heat_on(self):
"""Return true if auxiliary heater is on."""
boost_status = None
if self.device_type == "Heating":
boost_status = self.session.heating.get_boost(self.node_id)
elif self.device_type == "HotWater":
boost_status = self.session.hotwater.get_boost(self.node_id)
return boost_status == "ON"
def turn_aux_heat_on(self):
"""Turn auxiliary heater on."""
target_boost_time = 30
if self.device_type == "Heating":
curtemp = self.session.heating.current_temperature(self.node_id)
curtemp = round(curtemp * 2) / 2
target_boost_temperature = curtemp + 0.5
self.session.heating.turn_boost_on(self.node_id,
target_boost_time,
target_boost_temperature)
elif self.device_type == "HotWater":
self.session.hotwater.turn_boost_on(self.node_id,
target_boost_time)
for entity in self.session.entities:
entity.handle_update(self.data_updatesource)
def turn_aux_heat_off(self):
"""Turn auxiliary heater off."""
if self.device_type == "Heating":
self.session.heating.turn_boost_off(self.node_id)
elif self.device_type == "HotWater":
self.session.hotwater.turn_boost_off(self.node_id)
for entity in self.session.entities:
entity.handle_update(self.data_updatesource)
def update(self):
"""Update all Node data frome Hive."""
self.session.core.update_data(self.node_id)
+25 -7
View File
@@ -8,7 +8,8 @@ import logging
from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE)
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
from homeassistant.components.homematic import (
HMDevice, ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT)
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
DEPENDENCIES = ['homematic']
@@ -39,6 +40,7 @@ HM_HUMI_MAP = [
]
HM_CONTROL_MODE = 'CONTROL_MODE'
HM_IP_CONTROL_MODE = 'SET_POINT_MODE'
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
@@ -75,11 +77,25 @@ class HMThermostat(HMDevice, ClimateDevice):
if HM_CONTROL_MODE not in self._data:
return None
# read state and search
for mode, state in HM_STATE_MAP.items():
code = getattr(self._hmdevice, mode, 0)
if self._data.get('CONTROL_MODE') == code:
return state
set_point_mode = self._data.get('SET_POINT_MODE', -1)
control_mode = self._data.get('CONTROL_MODE', -1)
boost_mode = self._data.get('BOOST_MODE', False)
# boost mode is active
if boost_mode:
return STATE_BOOST
# HM ip etrv 2 uses the set_point_mode to say if its
# auto or manual
elif not set_point_mode == -1:
code = set_point_mode
# Other devices use the control_mode
else:
code = control_mode
# get the name of the mode
name = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][code]
return name.lower()
@property
def operation_list(self):
@@ -125,6 +141,7 @@ class HMThermostat(HMDevice, ClimateDevice):
if state == operation_mode:
code = getattr(self._hmdevice, mode, 0)
self._hmdevice.MODE = code
return
@property
def min_temp(self):
@@ -141,7 +158,8 @@ class HMThermostat(HMDevice, ClimateDevice):
self._state = next(iter(self._hmdevice.WRITENODE.keys()))
self._data[self._state] = STATE_UNKNOWN
if HM_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE:
if HM_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE or \
HM_IP_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE:
self._data[HM_CONTROL_MODE] = STATE_UNKNOWN
for node in self._hmdevice.SENSORNODE.keys():
+5
View File
@@ -159,6 +159,11 @@ class KNXClimate(ClimateDevice):
"""Return the name of the KNX device."""
return self.device.name
@property
def available(self):
"""Return True if entity is available."""
return self.hass.data[DATA_KNX].connected
@property
def should_poll(self):
"""No polling needed within KNX."""
+16 -6
View File
@@ -20,8 +20,9 @@ from homeassistant.components.climate import (
SUPPORT_AUX_HEAT)
from homeassistant.const import (
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME)
from homeassistant.components.mqtt import (CONF_QOS, CONF_RETAIN,
MQTT_BASE_PLATFORM_SCHEMA)
from homeassistant.components.mqtt import (
CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN, CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE, MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability)
import homeassistant.helpers.config_validation as cv
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
SPEED_HIGH)
@@ -93,7 +94,7 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean,
vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string,
vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string,
})
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine
@@ -134,19 +135,25 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
STATE_OFF, STATE_OFF, False,
config.get(CONF_SEND_IF_OFF),
config.get(CONF_PAYLOAD_ON),
config.get(CONF_PAYLOAD_OFF))
config.get(CONF_PAYLOAD_OFF),
config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE))
])
class MqttClimate(ClimateDevice):
class MqttClimate(MqttAvailability, ClimateDevice):
"""Representation of a demo climate device."""
def __init__(self, hass, name, topic, qos, retain, mode_list,
fan_mode_list, swing_mode_list, target_temperature, away,
hold, current_fan_mode, current_swing_mode,
current_operation, aux, send_if_off, payload_on,
payload_off):
payload_off, availability_topic, payload_available,
payload_not_available):
"""Initialize the climate device."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
self.hass = hass
self._name = name
self._topic = topic
@@ -169,8 +176,11 @@ class MqttClimate(ClimateDevice):
self._payload_on = payload_on
self._payload_off = payload_off
@asyncio.coroutine
def async_added_to_hass(self):
"""Handle being added to home assistant."""
yield from super().async_added_to_hass()
@callback
def handle_current_temp_received(topic, payload, qos):
"""Handle current temperature coming via MQTT."""
View File
-5
View File
@@ -79,11 +79,6 @@ class NetatmoThermostat(ClimateDevice):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._target_temperature
@property
def temperature_unit(self):
"""Return the unit of measurement."""
+227
View File
@@ -0,0 +1,227 @@
"""
Support for NuHeat thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.nuheat/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.components.climate import (
ClimateDevice,
DOMAIN,
SUPPORT_HOLD_MODE,
SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_TEMPERATURE,
STATE_AUTO,
STATE_HEAT,
STATE_IDLE)
from homeassistant.components.nuheat import DOMAIN as NUHEAT_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_TEMPERATURE,
TEMP_CELSIUS,
TEMP_FAHRENHEIT)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
DEPENDENCIES = ["nuheat"]
_LOGGER = logging.getLogger(__name__)
ICON = "mdi:thermometer"
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
# Hold modes
MODE_AUTO = STATE_AUTO # Run device schedule
MODE_HOLD_TEMPERATURE = "temperature"
MODE_TEMPORARY_HOLD = "temporary_temperature"
OPERATION_LIST = [STATE_HEAT, STATE_IDLE]
SCHEDULE_HOLD = 3
SCHEDULE_RUN = 1
SCHEDULE_TEMPORARY_HOLD = 2
SERVICE_RESUME_PROGRAM = "nuheat_resume_program"
RESUME_PROGRAM_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids
})
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_HOLD_MODE |
SUPPORT_OPERATION_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the NuHeat thermostat(s)."""
if discovery_info is None:
return
temperature_unit = hass.config.units.temperature_unit
api, serial_numbers = hass.data[NUHEAT_DOMAIN]
thermostats = [
NuHeatThermostat(api, serial_number, temperature_unit)
for serial_number in serial_numbers
]
add_devices(thermostats, True)
def resume_program_set_service(service):
"""Resume the program on the target thermostats."""
entity_id = service.data.get(ATTR_ENTITY_ID)
if entity_id:
target_thermostats = [device for device in thermostats
if device.entity_id in entity_id]
else:
target_thermostats = thermostats
for thermostat in target_thermostats:
thermostat.resume_program()
thermostat.schedule_update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service,
schema=RESUME_PROGRAM_SCHEMA)
class NuHeatThermostat(ClimateDevice):
"""Representation of a NuHeat Thermostat."""
def __init__(self, api, serial_number, temperature_unit):
"""Initialize the thermostat."""
self._thermostat = api.get_thermostat(serial_number)
self._temperature_unit = temperature_unit
self._force_update = False
@property
def name(self):
"""Return the name of the thermostat."""
return self._thermostat.room
@property
def icon(self):
"""Return the icon to use in the frontend."""
return ICON
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property
def temperature_unit(self):
"""Return the unit of measurement."""
if self._temperature_unit == "C":
return TEMP_CELSIUS
return TEMP_FAHRENHEIT
@property
def current_temperature(self):
"""Return the current temperature."""
if self._temperature_unit == "C":
return self._thermostat.celsius
return self._thermostat.fahrenheit
@property
def current_operation(self):
"""Return current operation. ie. heat, idle."""
if self._thermostat.heating:
return STATE_HEAT
return STATE_IDLE
@property
def min_temp(self):
"""Return the minimum supported temperature for the thermostat."""
if self._temperature_unit == "C":
return self._thermostat.min_celsius
return self._thermostat.min_fahrenheit
@property
def max_temp(self):
"""Return the maximum supported temperature for the thermostat."""
if self._temperature_unit == "C":
return self._thermostat.max_celsius
return self._thermostat.max_fahrenheit
@property
def target_temperature(self):
"""Return the currently programmed temperature."""
if self._temperature_unit == "C":
return self._thermostat.target_celsius
return self._thermostat.target_fahrenheit
@property
def current_hold_mode(self):
"""Return current hold mode."""
schedule_mode = self._thermostat.schedule_mode
if schedule_mode == SCHEDULE_RUN:
return MODE_AUTO
if schedule_mode == SCHEDULE_HOLD:
return MODE_HOLD_TEMPERATURE
if schedule_mode == SCHEDULE_TEMPORARY_HOLD:
return MODE_TEMPORARY_HOLD
return MODE_AUTO
@property
def operation_list(self):
"""Return list of possible operation modes."""
return OPERATION_LIST
def resume_program(self):
"""Resume the thermostat's programmed schedule."""
self._thermostat.resume_schedule()
self._force_update = True
def set_hold_mode(self, hold_mode, **kwargs):
"""Update the hold mode of the thermostat."""
if hold_mode == MODE_AUTO:
schedule_mode = SCHEDULE_RUN
if hold_mode == MODE_HOLD_TEMPERATURE:
schedule_mode = SCHEDULE_HOLD
if hold_mode == MODE_TEMPORARY_HOLD:
schedule_mode = SCHEDULE_TEMPORARY_HOLD
self._thermostat.schedule_mode = schedule_mode
self._force_update = True
def set_temperature(self, **kwargs):
"""Set a new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if self._temperature_unit == "C":
self._thermostat.target_celsius = temperature
else:
self._thermostat.target_fahrenheit = temperature
_LOGGER.debug(
"Setting NuHeat thermostat temperature to %s %s",
temperature, self.temperature_unit)
self._force_update = True
def update(self):
"""Get the latest state from the thermostat."""
if self._force_update:
self._throttled_update(no_throttle=True)
self._force_update = False
else:
self._throttled_update()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def _throttled_update(self, **kwargs):
"""Get the latest state from the thermostat with a throttle."""
self._thermostat.get_data()
+84 -20
View File
@@ -13,37 +13,49 @@ import async_timeout
import voluptuous as vol
from homeassistant.const import (
ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT)
ATTR_ENTITY_ID, ATTR_STATE, ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID,
STATE_ON, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from homeassistant.components.climate import (
ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA,
ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_SWING_MODE,
SUPPORT_AUX_HEAT)
SUPPORT_FAN_MODE, SUPPORT_SWING_MODE,
SUPPORT_ON_OFF)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.temperature import convert as convert_temperature
REQUIREMENTS = ['pysensibo==1.0.1']
REQUIREMENTS = ['pysensibo==1.0.2']
_LOGGER = logging.getLogger(__name__)
ALL = 'all'
TIMEOUT = 10
SERVICE_ASSUME_STATE = 'sensibo_assume_state'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_ID, default=ALL): vol.All(cv.ensure_list, [cv.string]),
})
ASSUME_STATE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_STATE): cv.string,
})
_FETCH_FIELDS = ','.join([
'room{name}', 'measurements', 'remoteCapabilities',
'acState', 'connectionStatus{isAlive}', 'temperatureUnit'])
_INITIAL_FETCH_FIELDS = 'id,' + _FETCH_FIELDS
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_FAN_MODE | SUPPORT_AWAY_MODE | SUPPORT_SWING_MODE |
SUPPORT_AUX_HEAT)
FIELD_TO_FLAG = {
'fanLevel': SUPPORT_FAN_MODE,
'mode': SUPPORT_OPERATION_MODE,
'swing': SUPPORT_SWING_MODE,
'targetTemperature': SUPPORT_TARGET_TEMPERATURE,
'on': SUPPORT_ON_OFF,
}
@asyncio.coroutine
@@ -68,6 +80,28 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if devices:
async_add_devices(devices)
@asyncio.coroutine
def async_assume_state(service):
"""Set state according to external service call.."""
entity_ids = service.data.get(ATTR_ENTITY_ID)
if entity_ids:
target_climate = [device for device in devices
if device.entity_id in entity_ids]
else:
target_climate = devices
update_tasks = []
for climate in target_climate:
yield from climate.async_assume_state(
service.data.get(ATTR_STATE))
update_tasks.append(climate.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_ASSUME_STATE, async_assume_state,
schema=ASSUME_STATE_SCHEMA)
class SensiboClimate(ClimateDevice):
"""Representation of a Sensibo device."""
@@ -80,12 +114,13 @@ class SensiboClimate(ClimateDevice):
"""
self._client = client
self._id = data['id']
self._external_state = None
self._do_update(data)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
return self._supported_features
def _do_update(self, data):
self._name = data['room']['name']
@@ -106,6 +141,15 @@ class SensiboClimate(ClimateDevice):
else:
self._temperature_unit = self.unit_of_measurement
self._temperatures_list = []
self._supported_features = 0
for key in self._ac_states:
if key in FIELD_TO_FLAG:
self._supported_features |= FIELD_TO_FLAG[key]
@property
def state(self):
"""Return the current state."""
return self._external_state or super().state
@property
def device_state_attributes(self):
@@ -188,7 +232,7 @@ class SensiboClimate(ClimateDevice):
return self._name
@property
def is_aux_heat_on(self):
def is_on(self):
"""Return true if AC is on."""
return self._ac_states['on']
@@ -196,13 +240,13 @@ class SensiboClimate(ClimateDevice):
def min_temp(self):
"""Return the minimum temperature."""
return self._temperatures_list[0] \
if len(self._temperatures_list) else super.min_temp()
if len(self._temperatures_list) else super().min_temp()
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._temperatures_list[-1] \
if len(self._temperatures_list) else super.max_temp()
if len(self._temperatures_list) else super().max_temp()
@asyncio.coroutine
def async_set_temperature(self, **kwargs):
@@ -226,42 +270,62 @@ class SensiboClimate(ClimateDevice):
with async_timeout.timeout(TIMEOUT):
yield from self._client.async_set_ac_state_property(
self._id, 'targetTemperature', temperature)
self._id, 'targetTemperature', temperature, self._ac_states)
@asyncio.coroutine
def async_set_fan_mode(self, fan):
"""Set new target fan mode."""
with async_timeout.timeout(TIMEOUT):
yield from self._client.async_set_ac_state_property(
self._id, 'fanLevel', fan)
self._id, 'fanLevel', fan, self._ac_states)
@asyncio.coroutine
def async_set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
with async_timeout.timeout(TIMEOUT):
yield from self._client.async_set_ac_state_property(
self._id, 'mode', operation_mode)
self._id, 'mode', operation_mode, self._ac_states)
@asyncio.coroutine
def async_set_swing_mode(self, swing_mode):
"""Set new target swing operation."""
with async_timeout.timeout(TIMEOUT):
yield from self._client.async_set_ac_state_property(
self._id, 'swing', swing_mode)
self._id, 'swing', swing_mode, self._ac_states)
@asyncio.coroutine
def async_turn_aux_heat_on(self):
def async_on(self):
"""Turn Sensibo unit on."""
with async_timeout.timeout(TIMEOUT):
yield from self._client.async_set_ac_state_property(
self._id, 'on', True)
self._id, 'on', True, self._ac_states)
@asyncio.coroutine
def async_turn_aux_heat_off(self):
def async_off(self):
"""Turn Sensibo unit on."""
with async_timeout.timeout(TIMEOUT):
yield from self._client.async_set_ac_state_property(
self._id, 'on', False)
self._id, 'on', False, self._ac_states)
@asyncio.coroutine
def async_assume_state(self, state):
"""Set external state."""
change_needed = (state != STATE_OFF and not self.is_on) \
or (state == STATE_OFF and self.is_on)
if change_needed:
with async_timeout.timeout(TIMEOUT):
yield from self._client.async_set_ac_state_property(
self._id,
'on',
state != STATE_OFF, # value
self._ac_states,
True # assumed_state
)
if state in [STATE_ON, STATE_OFF]:
self._external_state = None
else:
self._external_state = state
@asyncio.coroutine
def async_update(self):
+53 -1
View File
@@ -80,7 +80,22 @@ set_swing_mode:
example: 'climate.nest'
swing_mode:
description: New value of swing mode.
example: 1
example:
turn_on:
description: Turn climate device on.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'climate.kitchen'
turn_off:
description: Turn climate device off.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'climate.kitchen'
ecobee_set_fan_min_on_time:
description: Set the minimum fan on time.
fields:
@@ -100,3 +115,40 @@ ecobee_resume_program:
resume_all:
description: Resume all events and return to the scheduled program. This default to false which removes only the top event.
example: true
nuheat_resume_program:
description: Resume the programmed schedule.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'climate.kitchen'
econet_add_vacation:
description: Add a vacation to your water heater.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'climate.water_heater'
start_date:
description: The timestamp of when the vacation should start. (Optional, defaults to now)
example: 1513186320
end_date:
description: The timestamp of when the vacation should end.
example: 1513445520
econet_delete_vacation:
description: Delete your existing vacation from your water heater.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'climate.water_heater'
sensibo_assume_state:
description: Set Sensibo device to external state.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'climate.kitchen'
state:
description: State to set.
example: 'idle'
+6 -1
View File
@@ -6,7 +6,7 @@ https://home-assistant.io/components/climate.tado/
"""
import logging
from homeassistant.const import TEMP_CELSIUS
from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS)
from homeassistant.components.climate import (
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
from homeassistant.const import ATTR_TEMPERATURE
@@ -192,6 +192,11 @@ class TadoClimate(ClimateDevice):
"""Return true if away mode is on."""
return self._is_away
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
return PRECISION_TENTHS
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
@@ -0,0 +1,90 @@
"""
Platform for Roth Touchline heat pump controller.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/climate.touchline/
"""
import logging
import voluptuous as vol
from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import CONF_HOST, TEMP_CELSIUS, ATTR_TEMPERATURE
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pytouchline==0.6']
_LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Touchline devices."""
from pytouchline import PyTouchline
host = config[CONF_HOST]
py_touchline = PyTouchline()
number_of_devices = int(py_touchline.get_number_of_devices(host))
devices = []
for device_id in range(0, number_of_devices):
devices.append(Touchline(PyTouchline(device_id)))
add_devices(devices, True)
class Touchline(ClimateDevice):
"""Representation of a Touchline device."""
def __init__(self, touchline_thermostat):
"""Initialize the climate device."""
self.unit = touchline_thermostat
self._name = None
self._current_temperature = None
self._target_temperature = None
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
def update(self):
"""Update unit attributes."""
self.unit.update()
self._name = self.unit.get_name()
self._current_temperature = self.unit.get_current_temperature()
self._target_temperature = self.unit.get_target_temperature()
@property
def should_poll(self):
"""Return the polling state."""
return True
@property
def name(self):
"""Return the name of the climate device."""
return self._name
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
def set_temperature(self, **kwargs):
"""Set new target temperature."""
if kwargs.get(ATTR_TEMPERATURE) is not None:
self._target_temperature = kwargs.get(ATTR_TEMPERATURE)
self.unit.set_target_temperature(self._target_temperature)
View File
+158 -41
View File
@@ -5,13 +5,18 @@ import json
import logging
import os
import aiohttp
import async_timeout
import voluptuous as vol
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE)
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME, CONF_TYPE)
from homeassistant.helpers import entityfilter
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import dt as dt_util
from homeassistant.components.alexa import smart_home
from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import smart_home as ga_sh
from . import http_api, iot
from .const import CONFIG_DIR, DOMAIN, SERVERS
@@ -21,22 +26,46 @@ REQUIREMENTS = ['warrant==0.6.1']
_LOGGER = logging.getLogger(__name__)
CONF_ALEXA = 'alexa'
CONF_ALEXA_FILTER = 'filter'
CONF_GOOGLE_ACTIONS = 'google_actions'
CONF_FILTER = 'filter'
CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id'
CONF_ALIASES = 'aliases'
MODE_DEV = 'development'
DEFAULT_MODE = 'production'
DEPENDENCIES = ['http']
ALEXA_SCHEMA = vol.Schema({
CONF_ENTITY_CONFIG = 'entity_config'
ALEXA_ENTITY_SCHEMA = vol.Schema({
vol.Optional(alexa_sh.CONF_DESCRIPTION): cv.string,
vol.Optional(alexa_sh.CONF_DISPLAY_CATEGORIES): cv.string,
vol.Optional(alexa_sh.CONF_NAME): cv.string,
})
GOOGLE_ENTITY_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_TYPE): vol.In(ga_sh.MAPPING_COMPONENT),
vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string])
})
ASSISTANT_SCHEMA = vol.Schema({
vol.Optional(
CONF_ALEXA_FILTER,
CONF_FILTER,
default=lambda: entityfilter.generate_filter([], [], [], [])
): entityfilter.FILTER_SCHEMA,
})
ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend({
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}
})
GACTIONS_SCHEMA = ASSISTANT_SCHEMA.extend({
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: GOOGLE_ENTITY_SCHEMA}
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
@@ -46,7 +75,8 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_USER_POOL_ID): str,
vol.Optional(CONF_REGION): str,
vol.Optional(CONF_RELAYER): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
}),
}, extra=vol.ALLOW_EXTRA)
@@ -55,22 +85,26 @@ CONFIG_SCHEMA = vol.Schema({
def async_setup(hass, config):
"""Initialize the Home Assistant cloud."""
if DOMAIN in config:
kwargs = config[DOMAIN]
kwargs = dict(config[DOMAIN])
else:
kwargs = {CONF_MODE: DEFAULT_MODE}
if CONF_ALEXA not in kwargs:
kwargs[CONF_ALEXA] = ALEXA_SCHEMA({})
alexa_conf = kwargs.pop(CONF_ALEXA, None) or ALEXA_SCHEMA({})
if CONF_GOOGLE_ACTIONS not in kwargs:
kwargs[CONF_GOOGLE_ACTIONS] = GACTIONS_SCHEMA({})
kwargs[CONF_ALEXA] = alexa_sh.Config(
should_expose=alexa_conf[CONF_FILTER],
entity_config=alexa_conf.get(CONF_ENTITY_CONFIG),
)
kwargs[CONF_ALEXA] = smart_home.Config(**kwargs[CONF_ALEXA])
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
@asyncio.coroutine
def init_cloud(event):
"""Initialize connection."""
yield from cloud.initialize()
success = yield from cloud.initialize()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, init_cloud)
if not success:
return False
yield from http_api.async_setup(hass)
return True
@@ -79,12 +113,16 @@ def async_setup(hass, config):
class Cloud:
"""Store the configuration of the cloud connection."""
def __init__(self, hass, mode, cognito_client_id=None, user_pool_id=None,
region=None, relayer=None, alexa=None):
def __init__(self, hass, mode, alexa, google_actions,
cognito_client_id=None, user_pool_id=None, region=None,
relayer=None):
"""Create an instance of Cloud."""
self.hass = hass
self.mode = mode
self.alexa_config = alexa
self._google_actions = google_actions
self._gactions_config = None
self.jwt_keyset = None
self.id_token = None
self.access_token = None
self.refresh_token = None
@@ -104,11 +142,6 @@ class Cloud:
self.region = info['region']
self.relayer = info['relayer']
@property
def cognito_email_based(self):
"""Return if cognito is email based."""
return not self.user_pool_id.endswith('GmV')
@property
def is_logged_in(self):
"""Get if cloud is logged in."""
@@ -128,37 +161,44 @@ class Cloud:
@property
def claims(self):
"""Get the claims from the id token."""
from jose import jwt
return jwt.get_unverified_claims(self.id_token)
"""Return the claims from the id token."""
return self._decode_claims(self.id_token)
@property
def user_info_path(self):
"""Get path to the stored auth."""
return self.path('{}_auth.json'.format(self.mode))
@property
def gactions_config(self):
"""Return the Google Assistant config."""
if self._gactions_config is None:
conf = self._google_actions
def should_expose(entity):
"""If an entity should be exposed."""
return conf['filter'](entity.entity_id)
self._gactions_config = ga_sh.Config(
should_expose=should_expose,
agent_user_id=self.claims['cognito:username'],
entity_config=conf.get(CONF_ENTITY_CONFIG),
)
return self._gactions_config
@asyncio.coroutine
def initialize(self):
"""Initialize and load cloud info."""
def load_config():
"""Load the configuration."""
# Ensure config dir exists
path = self.hass.config.path(CONFIG_DIR)
if not os.path.isdir(path):
os.mkdir(path)
jwt_success = yield from self._fetch_jwt_keyset()
user_info = self.user_info_path
if os.path.isfile(user_info):
with open(user_info, 'rt') as file:
info = json.loads(file.read())
self.id_token = info['id_token']
self.access_token = info['access_token']
self.refresh_token = info['refresh_token']
if not jwt_success:
return False
yield from self.hass.async_add_job(load_config)
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START,
self._start_cloud)
if self.id_token is not None:
yield from self.iot.connect()
return True
def path(self, *parts):
"""Get config path inside cloud dir.
@@ -175,6 +215,7 @@ class Cloud:
self.id_token = None
self.access_token = None
self.refresh_token = None
self._gactions_config = None
yield from self.hass.async_add_job(
lambda: os.remove(self.user_info_path))
@@ -187,3 +228,79 @@ class Cloud:
'access_token': self.access_token,
'refresh_token': self.refresh_token,
}, indent=4))
def _start_cloud(self, event):
"""Start the cloud component."""
# Ensure config dir exists
path = self.hass.config.path(CONFIG_DIR)
if not os.path.isdir(path):
os.mkdir(path)
user_info = self.user_info_path
if not os.path.isfile(user_info):
return
with open(user_info, 'rt') as file:
info = json.loads(file.read())
# Validate tokens
try:
for token in 'id_token', 'access_token':
self._decode_claims(info[token])
except ValueError as err: # Raised when token is invalid
_LOGGER.warning('Found invalid token %s: %s', token, err)
return
self.id_token = info['id_token']
self.access_token = info['access_token']
self.refresh_token = info['refresh_token']
self.hass.add_job(self.iot.connect())
@asyncio.coroutine
def _fetch_jwt_keyset(self):
"""Fetch the JWT keyset for the Cognito instance."""
session = async_get_clientsession(self.hass)
url = ("https://cognito-idp.us-east-1.amazonaws.com/"
"{}/.well-known/jwks.json".format(self.user_pool_id))
try:
with async_timeout.timeout(10, loop=self.hass.loop):
req = yield from session.get(url)
self.jwt_keyset = yield from req.json()
return True
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
_LOGGER.error("Error fetching Cognito keyset: %s", err)
return False
def _decode_claims(self, token):
"""Decode the claims in a token."""
from jose import jwt, exceptions as jose_exceptions
try:
header = jwt.get_unverified_header(token)
except jose_exceptions.JWTError as err:
raise ValueError(str(err)) from None
kid = header.get("kid")
if kid is None:
raise ValueError('No kid in header')
# Locate the key for this kid
key = None
for key_dict in self.jwt_keyset["keys"]:
if key_dict["kid"] == kid:
key = key_dict
break
if not key:
raise ValueError(
"Unable to locate kid ({}) in keyset".format(kid))
try:
return jwt.decode(
token, key, audience=self.cognito_client_id, options={
'verify_exp': False,
})
except jose_exceptions.JWTError as err:
raise ValueError(str(err)) from None
+19 -23
View File
@@ -1,5 +1,4 @@
"""Package to communicate with the authentication API."""
import hashlib
import logging
@@ -58,11 +57,6 @@ def _map_aws_exception(err):
return ex(err.response['Error']['Message'])
def _generate_username(email):
"""Generate a username from an email address."""
return hashlib.sha512(email.encode('utf-8')).hexdigest()
def register(cloud, email, password):
"""Register a new account."""
from botocore.exceptions import ClientError
@@ -72,10 +66,7 @@ def register(cloud, email, password):
# https://github.com/capless/warrant/pull/82
cognito.add_base_attributes()
try:
if cloud.cognito_email_based:
cognito.register(email, password)
else:
cognito.register(_generate_username(email), password)
cognito.register(email, password)
except ClientError as err:
raise _map_aws_exception(err)
@@ -86,11 +77,22 @@ def confirm_register(cloud, confirmation_code, email):
cognito = _cognito(cloud)
try:
if cloud.cognito_email_based:
cognito.confirm_sign_up(confirmation_code, email)
else:
cognito.confirm_sign_up(confirmation_code,
_generate_username(email))
cognito.confirm_sign_up(confirmation_code, email)
except ClientError as err:
raise _map_aws_exception(err)
def resend_email_confirm(cloud, email):
"""Resend email confirmation."""
from botocore.exceptions import ClientError
cognito = _cognito(cloud, username=email)
try:
cognito.client.resend_confirmation_code(
Username=email,
ClientId=cognito.client_id
)
except ClientError as err:
raise _map_aws_exception(err)
@@ -99,10 +101,7 @@ def forgot_password(cloud, email):
"""Initiate forgotten password flow."""
from botocore.exceptions import ClientError
if cloud.cognito_email_based:
cognito = _cognito(cloud, username=email)
else:
cognito = _cognito(cloud, username=_generate_username(email))
cognito = _cognito(cloud, username=email)
try:
cognito.initiate_forgot_password()
@@ -114,10 +113,7 @@ def confirm_forgot_password(cloud, confirmation_code, email, new_password):
"""Confirm forgotten password code and change password."""
from botocore.exceptions import ClientError
if cloud.cognito_email_based:
cognito = _cognito(cloud, username=email)
else:
cognito = _cognito(cloud, username=_generate_username(email))
cognito = _cognito(cloud, username=email)
try:
cognito.confirm_forgot_password(confirmation_code, new_password)
+25 -1
View File
@@ -23,6 +23,7 @@ def async_setup(hass):
hass.http.register_view(CloudAccountView)
hass.http.register_view(CloudRegisterView)
hass.http.register_view(CloudConfirmRegisterView)
hass.http.register_view(CloudResendConfirmView)
hass.http.register_view(CloudForgotPasswordView)
hass.http.register_view(CloudConfirmForgotPasswordView)
@@ -172,6 +173,29 @@ class CloudConfirmRegisterView(HomeAssistantView):
return self.json_message('ok')
class CloudResendConfirmView(HomeAssistantView):
"""Resend email confirmation code."""
url = '/api/cloud/resend_confirm'
name = 'api:cloud:resend_confirm'
@_handle_cloud_errors
@RequestDataValidator(vol.Schema({
vol.Required('email'): str,
}))
@asyncio.coroutine
def post(self, request, data):
"""Handle resending confirm email code request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(
auth_api.resend_email_confirm, cloud, data['email'])
return self.json_message('ok')
class CloudForgotPasswordView(HomeAssistantView):
"""View to start Forgot Password flow.."""
@@ -228,6 +252,6 @@ def _account_data(cloud):
return {
'email': claims['email'],
'sub_exp': claims.get('custom:sub-exp'),
'sub_exp': claims['custom:sub-exp'],
'cloud': cloud.iot.state,
}
+15 -5
View File
@@ -5,7 +5,8 @@ import logging
from aiohttp import hdrs, client_exceptions, WSMsgType
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.components.alexa import smart_home
from homeassistant.components.alexa import smart_home as alexa
from homeassistant.components.google_assistant import smart_home as ga
from homeassistant.util.decorator import Registry
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import auth_api
@@ -78,7 +79,7 @@ class CloudIoT:
yield from hass.async_add_job(auth_api.check_token, self.cloud)
self.client = client = yield from session.ws_connect(
self.cloud.relayer, headers={
self.cloud.relayer, heartbeat=55, headers={
hdrs.AUTHORIZATION:
'Bearer {}'.format(self.cloud.id_token)
})
@@ -204,9 +205,18 @@ def async_handle_message(hass, cloud, handler_name, payload):
@asyncio.coroutine
def async_handle_alexa(hass, cloud, payload):
"""Handle an incoming IoT message for Alexa."""
return (yield from smart_home.async_handle_message(hass,
cloud.alexa_config,
payload))
result = yield from alexa.async_handle_message(hass, cloud.alexa_config,
payload)
return result
@HANDLERS.register('google_actions')
@asyncio.coroutine
def async_handle_google_actions(hass, cloud, payload):
"""Handle an incoming IoT message for Google Actions."""
result = yield from ga.async_handle_message(hass, cloud.gactions_config,
payload)
return result
@HANDLERS.register('cloud')
+90
View File
@@ -0,0 +1,90 @@
"""
Support for Coinbase.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/coinbase/
"""
from datetime import timedelta
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_API_KEY
from homeassistant.util import Throttle
from homeassistant.helpers.discovery import load_platform
REQUIREMENTS = ['coinbase==2.0.6']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'coinbase'
CONF_API_SECRET = 'api_secret'
CONF_EXCHANGE_CURRENCIES = 'exchange_rate_currencies'
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
DATA_COINBASE = 'coinbase_cache'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_API_SECRET): cv.string,
vol.Optional(CONF_EXCHANGE_CURRENCIES, default=[]):
vol.All(cv.ensure_list, [cv.string])
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the Coinbase component.
Will automatically setup sensors to support
wallets discovered on the network.
"""
api_key = config[DOMAIN].get(CONF_API_KEY)
api_secret = config[DOMAIN].get(CONF_API_SECRET)
exchange_currencies = config[DOMAIN].get(CONF_EXCHANGE_CURRENCIES)
hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData(api_key,
api_secret)
if not hasattr(coinbase_data, 'accounts'):
return False
for account in coinbase_data.accounts.data:
load_platform(hass, 'sensor', DOMAIN,
{'account': account}, config)
for currency in exchange_currencies:
if currency not in coinbase_data.exchange_rates.rates:
_LOGGER.warning("Currency %s not found", currency)
continue
native = coinbase_data.exchange_rates.currency
load_platform(hass,
'sensor',
DOMAIN,
{'native_currency': native,
'exchange_currency': currency},
config)
return True
class CoinbaseData(object):
"""Get the latest data and update the states."""
def __init__(self, api_key, api_secret):
"""Init the coinbase data object."""
from coinbase.wallet.client import Client
self.client = Client(api_key, api_secret)
self.update()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from coinbase."""
from coinbase.wallet.error import AuthenticationError
try:
self.accounts = self.client.get_accounts()
self.exchange_rates = self.client.get_exchange_rates()
except AuthenticationError as coinbase_error:
_LOGGER.error("Authentication error connecting"
" to coinbase: %s", coinbase_error)
+12 -11
View File
@@ -12,20 +12,27 @@ import warnings
import voluptuous as vol
from homeassistant import core
from homeassistant.loader import bind_hass
from homeassistant.components import http
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
from homeassistant.helpers import intent, config_validation as cv
from homeassistant.components import http
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import intent
from homeassistant.loader import bind_hass
REQUIREMENTS = ['fuzzywuzzy==0.16.0']
REQUIREMENTS = ['fuzzywuzzy==0.15.1']
DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__)
ATTR_TEXT = 'text'
DEPENDENCIES = ['http']
DOMAIN = 'conversation'
INTENT_TURN_OFF = 'HassTurnOff'
INTENT_TURN_ON = 'HassTurnOn'
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
REGEX_TYPE = type(re.compile(''))
SERVICE_PROCESS = 'process'
@@ -39,12 +46,6 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({
})
})}, extra=vol.ALLOW_EXTRA)
INTENT_TURN_ON = 'HassTurnOn'
INTENT_TURN_OFF = 'HassTurnOff'
REGEX_TYPE = type(re.compile(''))
_LOGGER = logging.getLogger(__name__)
@core.callback
@bind_hass
+3 -13
View File
@@ -6,12 +6,10 @@ at https://home-assistant.io/components/counter/
"""
import asyncio
import logging
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_ICON, CONF_NAME)
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
@@ -133,20 +131,12 @@ def async_setup(hass, config):
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml')
)
hass.services.async_register(
DOMAIN, SERVICE_INCREMENT, async_handler_service,
descriptions[SERVICE_INCREMENT], SERVICE_SCHEMA)
DOMAIN, SERVICE_INCREMENT, async_handler_service)
hass.services.async_register(
DOMAIN, SERVICE_DECREMENT, async_handler_service,
descriptions[SERVICE_DECREMENT], SERVICE_SCHEMA)
DOMAIN, SERVICE_DECREMENT, async_handler_service)
hass.services.async_register(
DOMAIN, SERVICE_RESET, async_handler_service,
descriptions[SERVICE_RESET], SERVICE_SCHEMA)
DOMAIN, SERVICE_RESET, async_handler_service)
yield from component.async_add_entities(entities)
return True
+1 -7
View File
@@ -8,11 +8,9 @@ import asyncio
from datetime import timedelta
import functools as ft
import logging
import os
import voluptuous as vol
from homeassistant.config import load_yaml_config_file
from homeassistant.loader import bind_hass
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity
@@ -179,16 +177,12 @@ def async_setup(hass, config):
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
for service_name in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[service_name].get(
'schema', COVER_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, service_name, async_handle_cover_service,
descriptions.get(service_name), schema=schema)
schema=schema)
return True
+13 -23
View File
@@ -8,8 +8,10 @@ import logging
from typing import Callable # noqa
from homeassistant.components.cover import CoverDevice, DOMAIN
import homeassistant.components.isy994 as isy
from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN
from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS,
ISYDevice)
from homeassistant.const import (
STATE_OPEN, STATE_CLOSED, STATE_OPENING, STATE_CLOSING, STATE_UNKNOWN)
from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -17,44 +19,32 @@ _LOGGER = logging.getLogger(__name__)
VALUE_TO_STATE = {
0: STATE_CLOSED,
101: STATE_UNKNOWN,
102: 'stopped',
103: STATE_CLOSING,
104: STATE_OPENING
}
UOM = ['97']
STATES = [STATE_OPEN, STATE_CLOSED, 'closing', 'opening', 'stopped']
# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 cover platform."""
if isy.ISY is None or not isy.ISY.connected:
_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 hass.data[ISY994_NODES][DOMAIN]:
devices.append(ISYCoverDevice(node))
for program in isy.PROGRAMS.get(DOMAIN, []):
try:
status = program[isy.KEY_STATUS]
actions = program[isy.KEY_ACTIONS]
assert actions.dtype == 'program', 'Not a program'
except (KeyError, AssertionError):
pass
else:
devices.append(ISYCoverProgram(program.name, status, actions))
for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]:
devices.append(ISYCoverProgram(name, status, actions))
add_devices(devices)
class ISYCoverDevice(isy.ISYDevice, CoverDevice):
class ISYCoverDevice(ISYDevice, CoverDevice):
"""Representation of an ISY994 cover device."""
def __init__(self, node: object):
"""Initialize the ISY994 cover device."""
isy.ISYDevice.__init__(self, node)
super().__init__(node)
@property
def current_cover_position(self) -> int:
@@ -90,7 +80,7 @@ class ISYCoverProgram(ISYCoverDevice):
def __init__(self, name: str, node: object, actions: object) -> None:
"""Initialize the ISY994 cover program."""
ISYCoverDevice.__init__(self, node)
super().__init__(node)
self._name = name
self._actions = actions
+5
View File
@@ -124,6 +124,11 @@ class KNXCover(CoverDevice):
"""Return the name of the KNX device."""
return self.device.name
@property
def available(self):
"""Return True if entity is available."""
return self.hass.data[DATA_KNX].connected
@property
def should_poll(self):
"""No polling needed within KNX."""
+9 -25
View File
@@ -21,8 +21,9 @@ from homeassistant.const import (
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN,
STATE_CLOSED, STATE_UNKNOWN)
from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_AVAILABILITY_TOPIC,
CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic)
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN,
valid_publish_topic, valid_subscribe_topic, MqttAvailability)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -37,8 +38,6 @@ CONF_SET_POSITION_TEMPLATE = 'set_position_template'
CONF_PAYLOAD_OPEN = 'payload_open'
CONF_PAYLOAD_CLOSE = 'payload_close'
CONF_PAYLOAD_STOP = 'payload_stop'
CONF_PAYLOAD_AVAILABLE = 'payload_available'
CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available'
CONF_STATE_OPEN = 'state_open'
CONF_STATE_CLOSED = 'state_closed'
CONF_TILT_CLOSED_POSITION = 'tilt_closed_value'
@@ -52,8 +51,6 @@ DEFAULT_NAME = 'MQTT Cover'
DEFAULT_PAYLOAD_OPEN = 'OPEN'
DEFAULT_PAYLOAD_CLOSE = 'CLOSE'
DEFAULT_PAYLOAD_STOP = 'STOP'
DEFAULT_PAYLOAD_AVAILABLE = 'online'
DEFAULT_PAYLOAD_NOT_AVAILABLE = 'offline'
DEFAULT_OPTIMISTIC = False
DEFAULT_RETAIN = False
DEFAULT_TILT_CLOSED_POSITION = 0
@@ -73,16 +70,11 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SET_POSITION_TEMPLATE, default=None): cv.template,
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_AVAILABILITY_TOPIC, default=None): valid_subscribe_topic,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string,
vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string,
vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string,
vol.Optional(CONF_PAYLOAD_AVAILABLE,
default=DEFAULT_PAYLOAD_AVAILABLE): cv.string,
vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE,
default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string,
vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string,
vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
@@ -98,7 +90,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
default=DEFAULT_TILT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_TILT_INVERT_STATE,
default=DEFAULT_TILT_INVERT_STATE): cv.boolean,
})
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine
@@ -143,7 +135,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
)])
class MqttCover(CoverDevice):
class MqttCover(MqttAvailability, CoverDevice):
"""Representation of a cover that can be controlled using MQTT."""
def __init__(self, name, state_topic, command_topic, availability_topic,
@@ -154,21 +146,19 @@ class MqttCover(CoverDevice):
tilt_closed_position, tilt_min, tilt_max, tilt_optimistic,
tilt_invert, position_topic, set_position_template):
"""Initialize the cover."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
self._position = None
self._state = None
self._name = name
self._state_topic = state_topic
self._command_topic = command_topic
self._availability_topic = availability_topic
self._available = True if availability_topic is None else False
self._tilt_command_topic = tilt_command_topic
self._tilt_status_topic = tilt_status_topic
self._qos = qos
self._payload_open = payload_open
self._payload_close = payload_close
self._payload_stop = payload_stop
self._payload_available = payload_available
self._payload_not_available = payload_not_available
self._state_open = state_open
self._state_closed = state_closed
self._retain = retain
@@ -186,10 +176,9 @@ class MqttCover(CoverDevice):
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe MQTT events.
"""Subscribe MQTT events."""
yield from super().async_added_to_hass()
This method is a coroutine.
"""
@callback
def tilt_updated(topic, payload, qos):
"""Handle tilt updates."""
@@ -266,11 +255,6 @@ class MqttCover(CoverDevice):
"""Return the name of the cover."""
return self._name
@property
def available(self) -> bool:
"""Return if cover is available."""
return self._available
@property
def is_closed(self):
"""Return if the cover is closed."""
+19 -2
View File
@@ -4,12 +4,29 @@ Support for RFXtrx cover components.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/cover.rfxtrx/
"""
import voluptuous as vol
import homeassistant.components.rfxtrx as rfxtrx
from homeassistant.components.cover import CoverDevice
from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME
from homeassistant.components.rfxtrx import (
CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT, DEFAULT_SIGNAL_REPETITIONS,
CONF_SIGNAL_REPETITIONS, CONF_DEVICES)
from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ['rfxtrx']
PLATFORM_SCHEMA = rfxtrx.DEFAULT_SCHEMA
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DEVICES, default={}): {
cv.string: vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean
})
},
vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean,
vol.Optional(CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS):
vol.Coerce(int),
})
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
+15 -1
View File
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.tahoma/
"""
import logging
from datetime import timedelta
from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT
from homeassistant.components.tahoma import (
@@ -14,6 +15,8 @@ DEPENDENCIES = ['tahoma']
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=60)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Tahoma covers."""
@@ -70,4 +73,15 @@ class TahomaCover(TahomaDevice, CoverDevice):
def stop_cover(self, **kwargs):
"""Stop the cover."""
self.apply_action('stopIdentify')
if self.tahoma_device.type == \
'io:RollerShutterWithLowSpeedManagementIOComponent':
self.apply_action('setPosition', 'secured')
else:
self.apply_action('stopIdentify')
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
if self.tahoma_device.type == 'io:WindowOpenerVeluxIOComponent':
return 'window'
else:
return None
View File
+6 -1
View File
@@ -63,10 +63,15 @@ COVER_SCHEMA = vol.Schema({
vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean,
vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_FRIENDLY_NAME, default=None): cv.string,
vol.Optional(CONF_FRIENDLY_NAME): cv.string,
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
})
COVER_SCHEMA = vol.All(
cv.deprecated(CONF_ENTITY_ID),
COVER_SCHEMA,
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
})
@@ -41,7 +41,7 @@ class XiaomiGenericCover(XiaomiDevice, CoverDevice):
@property
def is_closed(self):
"""Return if the cover is closed."""
return self.current_cover_position < 0
return self.current_cover_position <= 0
def close_cover(self, **kwargs):
"""Close the cover."""
+138
View File
@@ -0,0 +1,138 @@
"""
Platform for the Daikin AC.
For more details about this component, please refer to the documentation
https://home-assistant.io/components/daikin/
"""
import logging
from datetime import timedelta
from socket import timeout
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.discovery import SERVICE_DAIKIN
from homeassistant.const import (
CONF_HOSTS, CONF_ICON, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_TYPE
)
from homeassistant.helpers import discovery
from homeassistant.helpers.discovery import load_platform
from homeassistant.util import Throttle
REQUIREMENTS = ['pydaikin==0.4']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'daikin'
HTTP_RESOURCES = ['aircon/get_sensor_info', 'aircon/get_control_info']
ATTR_TARGET_TEMPERATURE = 'target_temperature'
ATTR_INSIDE_TEMPERATURE = 'inside_temperature'
ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
COMPONENT_TYPES = ['climate', 'sensor']
SENSOR_TYPE_TEMPERATURE = 'temperature'
SENSOR_TYPES = {
ATTR_INSIDE_TEMPERATURE: {
CONF_NAME: 'Inside Temperature',
CONF_ICON: 'mdi:thermometer',
CONF_TYPE: SENSOR_TYPE_TEMPERATURE
},
ATTR_OUTSIDE_TEMPERATURE: {
CONF_NAME: 'Outside Temperature',
CONF_ICON: 'mdi:thermometer',
CONF_TYPE: SENSOR_TYPE_TEMPERATURE
}
}
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(
CONF_HOSTS, default=[]
): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(
CONF_MONITORED_CONDITIONS,
default=list(SENSOR_TYPES.keys())
): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)])
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Establish connection with Daikin."""
def discovery_dispatch(service, discovery_info):
"""Dispatcher for Daikin discovery events."""
host = discovery_info.get('ip')
if daikin_api_setup(hass, host) is None:
return
for component in COMPONENT_TYPES:
load_platform(hass, component, DOMAIN, discovery_info,
config)
discovery.listen(hass, SERVICE_DAIKIN, discovery_dispatch)
for host in config.get(DOMAIN, {}).get(CONF_HOSTS, []):
if daikin_api_setup(hass, host) is None:
continue
discovery_info = {
'ip': host,
CONF_MONITORED_CONDITIONS:
config[DOMAIN][CONF_MONITORED_CONDITIONS]
}
load_platform(hass, 'sensor', DOMAIN, discovery_info, config)
return True
def daikin_api_setup(hass, host, name=None):
"""Create a Daikin instance only once."""
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
api = hass.data[DOMAIN].get(host)
if api is None:
from pydaikin import appliance
try:
device = appliance.Appliance(host)
except timeout:
_LOGGER.error("Connection to Daikin could not be established")
return False
if name is None:
name = device.values['name']
api = DaikinApi(device, name)
return api
class DaikinApi(object):
"""Keep the Daikin instance in one place and centralize the update."""
def __init__(self, device, name):
"""Initialize the Daikin Handle."""
self.device = device
self.name = name
self.ip_address = device.ip
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self, **kwargs):
"""Pull the latest data from Daikin."""
try:
for resource in HTTP_RESOURCES:
self.device.values.update(
self.device.get_resource(resource)
)
except timeout:
_LOGGER.warning(
"Connection failed for %s", self.ip_address
)
+170
View File
@@ -0,0 +1,170 @@
"""
Support for deCONZ devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/deconz/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.const import (
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.components.discovery import SERVICE_DECONZ
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.json import load_json, save_json
REQUIREMENTS = ['pydeconz==23']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'deconz'
CONFIG_FILE = 'deconz.conf'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_API_KEY): cv.string,
vol.Optional(CONF_PORT, default=80): cv.port,
})
}, extra=vol.ALLOW_EXTRA)
SERVICE_FIELD = 'field'
SERVICE_DATA = 'data'
SERVICE_SCHEMA = vol.Schema({
vol.Required(SERVICE_FIELD): cv.string,
vol.Required(SERVICE_DATA): cv.string,
})
CONFIG_INSTRUCTIONS = """
Unlock your deCONZ gateway to register with Home Assistant.
1. [Go to deCONZ system settings](http://{}:{}/edit_system.html)
2. Press "Unlock Gateway" button
[deCONZ platform documentation](https://home-assistant.io/components/deconz/)
"""
@asyncio.coroutine
def async_setup(hass, config):
"""Setup services and configuration for deCONZ component."""
result = False
config_file = yield from hass.async_add_job(
load_json, hass.config.path(CONFIG_FILE))
@asyncio.coroutine
def async_deconz_discovered(service, discovery_info):
"""Called when deCONZ gateway has been found."""
deconz_config = {}
deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
yield from async_request_configuration(hass, config, deconz_config)
if config_file:
result = yield from async_setup_deconz(hass, config, config_file)
if not result and DOMAIN in config and CONF_HOST in config[DOMAIN]:
deconz_config = config[DOMAIN]
if CONF_API_KEY in deconz_config:
result = yield from async_setup_deconz(hass, config, deconz_config)
else:
yield from async_request_configuration(hass, config, deconz_config)
return True
if not result:
discovery.async_listen(hass, SERVICE_DECONZ, async_deconz_discovered)
return True
@asyncio.coroutine
def async_setup_deconz(hass, config, deconz_config):
"""Setup deCONZ session.
Load config, group, light and sensor data for server information.
Start websocket for push notification of state changes from deCONZ.
"""
from pydeconz import DeconzSession
websession = async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, websession, **deconz_config)
result = yield from deconz.async_load_parameters()
if result is False:
_LOGGER.error("Failed to communicate with deCONZ.")
return False
hass.data[DOMAIN] = deconz
for component in ['binary_sensor', 'light', 'scene', 'sensor']:
hass.async_add_job(discovery.async_load_platform(
hass, component, DOMAIN, {}, config))
deconz.start()
@asyncio.coroutine
def async_configure(call):
"""Set attribute of device in deCONZ.
Field is a string representing a specific device in deCONZ
e.g. field='/lights/1/state'.
Data is a json object with what data you want to alter
e.g. data={'on': true}.
{
"field": "/lights/1/state",
"data": {"on": true}
}
See Dresden Elektroniks REST API documentation for details:
http://dresden-elektronik.github.io/deconz-rest-doc/rest/
"""
deconz = hass.data[DOMAIN]
field = call.data.get(SERVICE_FIELD)
data = call.data.get(SERVICE_DATA)
yield from deconz.async_put_state(field, data)
hass.services.async_register(
DOMAIN, 'configure', async_configure,
schema=SERVICE_SCHEMA)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz.close)
return True
@asyncio.coroutine
def async_request_configuration(hass, config, deconz_config):
"""Request configuration steps from the user."""
configurator = hass.components.configurator
@asyncio.coroutine
def async_configuration_callback(data):
"""Set up actions to do when our configuration callback is called."""
from pydeconz.utils import async_get_api_key
api_key = yield from async_get_api_key(hass.loop, **deconz_config)
if api_key:
deconz_config[CONF_API_KEY] = api_key
result = yield from async_setup_deconz(hass, config, deconz_config)
if result:
yield from hass.async_add_job(save_json,
hass.config.path(CONFIG_FILE),
deconz_config)
configurator.async_request_done(request_id)
return
else:
configurator.async_notify_errors(
request_id, "Couldn't load configuration.")
else:
configurator.async_notify_errors(
request_id, "Couldn't get an API key.")
return
instructions = CONFIG_INSTRUCTIONS.format(
deconz_config[CONF_HOST], deconz_config[CONF_PORT])
request_id = configurator.async_request_config(
"deCONZ", async_configuration_callback,
description=instructions,
entity_picture="/static/images/logo_deconz.jpeg",
submit_caption="I have unlocked the gateway",
)
@@ -0,0 +1,10 @@
configure:
description: Set attribute of device in Deconz. See Dresden Elektroniks REST API documentation for details http://dresden-elektronik.github.io/deconz-rest-doc/rest/
fields:
field:
description: Field is a string representing a specific device in Deconz.
example: '/lights/1/state'
data:
description: Data is a json object with what data you want to alter.
example: '{"on": true}'
@@ -7,7 +7,6 @@ https://home-assistant.io/components/device_tracker/
import asyncio
from datetime import timedelta
import logging
import os
from typing import Any, List, Sequence, Callable
import aiohttp
@@ -81,6 +80,8 @@ ATTR_VENDOR = 'vendor'
SOURCE_TYPE_GPS = 'gps'
SOURCE_TYPE_ROUTER = 'router'
SOURCE_TYPE_BLUETOOTH = 'bluetooth'
SOURCE_TYPE_BLUETOOTH_LE = 'bluetooth_le'
NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(None, vol.Schema({
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
@@ -88,7 +89,7 @@ NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(None, vol.Schema({
}))
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SCAN_INTERVAL): cv.time_period,
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
vol.Optional(CONF_TRACK_NEW): cv.boolean,
vol.Optional(CONF_CONSIDER_HOME,
default=DEFAULT_CONSIDER_HOME): vol.All(
cv.time_period, cv.positive_timedelta),
@@ -131,8 +132,11 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
conf = config.get(DOMAIN, [])
conf = conf[0] if conf else {}
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
defaults = conf.get(CONF_NEW_DEVICE_DEFAULTS, {})
track_new = conf.get(CONF_TRACK_NEW)
if track_new is None:
track_new = defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
devices = yield from async_load_config(yaml_path, hass, consider_home)
tracker = DeviceTracker(
@@ -204,12 +208,7 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)}
yield from tracker.async_see(**args)
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml')
)
hass.services.async_register(
DOMAIN, SERVICE_SEE, async_see_service, descriptions.get(SERVICE_SEE))
hass.services.async_register(DOMAIN, SERVICE_SEE, async_see_service)
# restore
yield from tracker.async_setup_tracked_device()
@@ -227,7 +226,8 @@ class DeviceTracker(object):
self.devices = {dev.dev_id: dev for dev in devices}
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
self.consider_home = consider_home
self.track_new = defaults.get(CONF_TRACK_NEW, track_new)
self.track_new = track_new if track_new is not None \
else defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
self.defaults = defaults
self.group = None
self._is_updating = asyncio.Lock(loop=hass.loop)
@@ -67,6 +67,15 @@ _IP_NEIGH_REGEX = re.compile(
r'\s?(router)?'
r'(?P<status>(\w+))')
_ARP_CMD = 'arp -n'
_ARP_REGEX = re.compile(
r'.+\s' +
r'\((?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\)\s' +
r'.+\s' +
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))' +
r'\s' +
r'.*')
# pylint: disable=unused-argument
def get_scanner(hass, config):
@@ -76,7 +85,22 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases')
def _parse_lines(lines, regex):
"""Parse the lines using the given regular expression.
If a line can't be parsed it is logged and skipped in the output.
"""
results = []
for line in lines:
match = regex.search(line)
if not match:
_LOGGER.debug("Could not parse row: %s", line)
continue
results.append(match.groupdict())
return results
Device = namedtuple('Device', ['mac', 'ip', 'name'])
class AsusWrtDeviceScanner(DeviceScanner):
@@ -121,16 +145,13 @@ class AsusWrtDeviceScanner(DeviceScanner):
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return [client['mac'] for client in self.last_results]
return list(self.last_results.keys())
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
if not self.last_results:
if device not in self.last_results:
return None
for client in self.last_results:
if client['mac'] == device:
return client['host']
return None
return self.last_results[device].name
def _update_info(self):
"""Ensure the information from the ASUSWRT router is up to date.
@@ -145,74 +166,88 @@ class AsusWrtDeviceScanner(DeviceScanner):
if not data:
return False
active_clients = [client for client in data.values() if
client['status'] == 'REACHABLE' or
client['status'] == 'DELAY' or
client['status'] == 'STALE' or
client['status'] == 'IN_ASSOCLIST']
self.last_results = active_clients
self.last_results = data
return True
def get_asuswrt_data(self):
"""Retrieve data from ASUSWRT and return parsed result."""
result = self.connection.get_result()
if not result:
return {}
"""Retrieve data from ASUSWRT.
Calls various commands on the router and returns the superset of all
responses. Some commands will not work on some routers.
"""
devices = {}
if self.mode == 'ap':
for lease in result.leases:
match = _WL_REGEX.search(lease.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse wl row: %s", lease)
continue
host = ''
devices[match.group('mac').upper()] = {
'host': host,
'status': 'IN_ASSOCLIST',
'ip': '',
'mac': match.group('mac').upper(),
}
else:
for lease in result.leases:
if lease.startswith(b'duid '):
continue
match = _LEASES_REGEX.search(lease.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse lease row: %s", lease)
continue
# For leases where the client doesn't set a hostname, ensure it
# is blank and not '*', which breaks entity_id down the line.
host = match.group('host')
if host == '*':
host = ''
devices[match.group('mac')] = {
'host': host,
'status': '',
'ip': match.group('ip'),
'mac': match.group('mac').upper(),
}
for neighbor in result.neighbors:
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse neighbor row: %s",
neighbor)
continue
if match.group('mac') in devices:
devices[match.group('mac')]['status'] = (
match.group('status'))
devices.update(self._get_wl())
devices = self._get_arp(devices)
devices = self._get_neigh(devices)
if not self.mode == 'ap':
devices.update(self._get_leases(devices))
return devices
def _get_wl(self):
lines = self.connection.run_command(_WL_CMD)
if not lines:
return {}
result = _parse_lines(lines, _WL_REGEX)
devices = {}
for device in result:
mac = device['mac'].upper()
devices[mac] = Device(mac, None, None)
return devices
def _get_leases(self, cur_devices):
lines = self.connection.run_command(_LEASES_CMD)
if not lines:
return {}
lines = [line for line in lines if not line.startswith('duid ')]
result = _parse_lines(lines, _LEASES_REGEX)
devices = {}
for device in result:
# For leases where the client doesn't set a hostname, ensure it
# is blank and not '*', which breaks entity_id down the line.
host = device['host']
if host == '*':
host = ''
mac = device['mac'].upper()
if mac in cur_devices:
devices[mac] = Device(mac, device['ip'], host)
return devices
def _get_neigh(self, cur_devices):
lines = self.connection.run_command(_IP_NEIGH_CMD)
if not lines:
return {}
result = _parse_lines(lines, _IP_NEIGH_REGEX)
devices = {}
for device in result:
if device['mac']:
mac = device['mac'].upper()
devices[mac] = Device(mac, None, None)
else:
cur_devices = {
k: v for k, v in
cur_devices.items() if v.ip != device['ip']
}
cur_devices.update(devices)
return cur_devices
def _get_arp(self, cur_devices):
lines = self.connection.run_command(_ARP_CMD)
if not lines:
return {}
result = _parse_lines(lines, _ARP_REGEX)
devices = {}
for device in result:
if device['mac']:
mac = device['mac'].upper()
devices[mac] = Device(mac, device['ip'], None)
else:
cur_devices = {
k: v for k, v in
cur_devices.items() if v.ip != device['ip']
}
cur_devices.update(devices)
return cur_devices
class _Connection:
def __init__(self):
@@ -247,8 +282,8 @@ class SshConnection(_Connection):
self._ssh_key = ssh_key
self._ap = ap
def get_result(self):
"""Retrieve a single AsusWrtResult through an SSH connection.
def run_command(self, command):
"""Run commands through an SSH connection.
Connect to the SSH server if not currently connected, otherwise
use the existing connection.
@@ -258,19 +293,10 @@ class SshConnection(_Connection):
try:
if not self.connected:
self.connect()
if self._ap:
neighbors = ['']
self._ssh.sendline(_WL_CMD)
self._ssh.prompt()
leases_result = self._ssh.before.split(b'\n')[1:-1]
else:
self._ssh.sendline(_IP_NEIGH_CMD)
self._ssh.prompt()
neighbors = self._ssh.before.split(b'\n')[1:-1]
self._ssh.sendline(_LEASES_CMD)
self._ssh.prompt()
leases_result = self._ssh.before.split(b'\n')[1:-1]
return AsusWrtResult(neighbors, leases_result)
self._ssh.sendline(command)
self._ssh.prompt()
lines = self._ssh.before.split(b'\n')[1:-1]
return [line.decode('utf-8') for line in lines]
except exceptions.EOF as err:
_LOGGER.error("Connection refused. SSH enabled?")
self.disconnect()
@@ -326,8 +352,8 @@ class TelnetConnection(_Connection):
self._ap = ap
self._prompt_string = None
def get_result(self):
"""Retrieve a single AsusWrtResult through a Telnet connection.
def run_command(self, command):
"""Run a command through a Telnet connection.
Connect to the Telnet server if not currently connected, otherwise
use the existing connection.
@@ -336,18 +362,10 @@ class TelnetConnection(_Connection):
if not self.connected:
self.connect()
self._telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii'))
neighbors = (self._telnet.read_until(self._prompt_string).
split(b'\n')[1:-1])
if self._ap:
self._telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
leases_result = (self._telnet.read_until(self._prompt_string).
split(b'\n')[1:-1])
else:
self._telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
leases_result = (self._telnet.read_until(self._prompt_string).
split(b'\n')[1:-1])
return AsusWrtResult(neighbors, leases_result)
self._telnet.write('{}\n'.format(command).encode('ascii'))
data = (self._telnet.read_until(self._prompt_string).
split(b'\n')[1:-1])
return [line.decode('utf-8') for line in data]
except EOFError:
_LOGGER.error("Unexpected response from router")
self.disconnect()
@@ -10,7 +10,7 @@ import voluptuous as vol
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.components.device_tracker import (
YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
PLATFORM_SCHEMA, load_config
PLATFORM_SCHEMA, load_config, SOURCE_TYPE_BLUETOOTH_LE
)
import homeassistant.util.dt as dt_util
import homeassistant.helpers.config_validation as cv
@@ -54,7 +54,8 @@ def setup_scanner(hass, config, see, discovery_info=None):
new_devices[address] = 1
return
see(mac=BLE_PREFIX + address, host_name=name.strip("\x00"))
see(mac=BLE_PREFIX + address, host_name=name.strip("\x00"),
source_type=SOURCE_TYPE_BLUETOOTH_LE)
def discover_ble_devices():
"""Discover Bluetooth LE devices."""
@@ -12,7 +12,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.components.device_tracker import (
YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
load_config, PLATFORM_SCHEMA, DEFAULT_TRACK_NEW)
load_config, PLATFORM_SCHEMA, DEFAULT_TRACK_NEW, SOURCE_TYPE_BLUETOOTH)
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -33,7 +33,8 @@ def setup_scanner(hass, config, see, discovery_info=None):
def see_device(device):
"""Mark a device as seen."""
see(mac=BT_PREFIX + device[0], host_name=device[1])
see(mac=BT_PREFIX + device[0], host_name=device[1],
source_type=SOURCE_TYPE_BLUETOOTH)
def discover_devices():
"""Discover Bluetooth devices."""
View File
@@ -5,23 +5,37 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.gpslogger/
"""
import asyncio
from functools import partial
import logging
from hmac import compare_digest
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY
from homeassistant.components.http import HomeAssistantView
from aiohttp.web import Request, HTTPUnauthorized # NOQA
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_PASSWORD, HTTP_UNPROCESSABLE_ENTITY
)
from homeassistant.components.http import (
CONF_API_PASSWORD, HomeAssistantView
)
# pylint: disable=unused-import
from homeassistant.components.device_tracker import ( # NOQA
DOMAIN, PLATFORM_SCHEMA)
DOMAIN, PLATFORM_SCHEMA
)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PASSWORD): cv.string,
})
def setup_scanner(hass, config, see, discovery_info=None):
@asyncio.coroutine
def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up an endpoint for the GPSLogger application."""
hass.http.register_view(GPSLoggerView(see))
hass.http.register_view(GPSLoggerView(async_see, config))
return True
@@ -32,26 +46,36 @@ class GPSLoggerView(HomeAssistantView):
url = '/api/gpslogger'
name = 'api:gpslogger'
def __init__(self, see):
def __init__(self, async_see, config):
"""Initialize GPSLogger url endpoints."""
self.see = see
self.async_see = async_see
self._password = config.get(CONF_PASSWORD)
# this component does not require external authentication if
# password is set
self.requires_auth = self._password is None
@asyncio.coroutine
def get(self, request):
def get(self, request: Request):
"""Handle for GPSLogger message received as GET."""
res = yield from self._handle(request.app['hass'], request.query)
return res
hass = request.app['hass']
data = request.query
if self._password is not None:
authenticated = CONF_API_PASSWORD in data and compare_digest(
self._password,
data[CONF_API_PASSWORD]
)
if not authenticated:
raise HTTPUnauthorized()
@asyncio.coroutine
def _handle(self, hass, data):
"""Handle GPSLogger requests."""
if 'latitude' not in data or 'longitude' not in data:
return ('Latitude and longitude not specified.',
HTTP_UNPROCESSABLE_ENTITY)
if 'device' not in data:
_LOGGER.error("Device id not specified")
return ('Device id not specified.', HTTP_UNPROCESSABLE_ENTITY)
return ('Device id not specified.',
HTTP_UNPROCESSABLE_ENTITY)
device = data['device'].replace('-', '')
gps_location = (data['latitude'], data['longitude'])
@@ -75,10 +99,11 @@ class GPSLoggerView(HomeAssistantView):
if 'activity' in data:
attrs['activity'] = data['activity']
yield from hass.async_add_job(
partial(self.see, dev_id=device,
gps=gps_location, battery=battery,
gps_accuracy=accuracy,
attributes=attrs))
hass.async_add_job(self.async_see(
dev_id=device,
gps=gps_location, battery=battery,
gps_accuracy=accuracy,
attributes=attrs
))
return 'Setting location for {}'.format(device)
@@ -32,19 +32,27 @@ CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
CONF_SECRET = 'secret'
CONF_WAYPOINT_IMPORT = 'waypoints'
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'
CONF_MQTT_TOPIC = 'mqtt_topic'
CONF_REGION_MAPPING = 'region_mapping'
CONF_EVENTS_ONLY = 'events_only'
DEPENDENCIES = ['mqtt']
OWNTRACKS_TOPIC = 'owntracks/#'
DEFAULT_OWNTRACKS_TOPIC = 'owntracks/#'
REGION_MAPPING = {}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float),
vol.Optional(CONF_WAYPOINT_IMPORT, default=True): cv.boolean,
vol.Optional(CONF_EVENTS_ONLY, default=False): cv.boolean,
vol.Optional(CONF_MQTT_TOPIC, default=DEFAULT_OWNTRACKS_TOPIC):
mqtt.valid_subscribe_topic,
vol.Optional(CONF_WAYPOINT_WHITELIST): vol.All(
cv.ensure_list, [cv.string]),
vol.Optional(CONF_SECRET): vol.Any(
vol.Schema({vol.Optional(cv.string): cv.string}),
cv.string)
cv.string),
vol.Optional(CONF_REGION_MAPPING, default=REGION_MAPPING): dict
})
@@ -82,31 +90,39 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
yield from async_handle_message(hass, context, message)
yield from mqtt.async_subscribe(
hass, OWNTRACKS_TOPIC, async_handle_mqtt_message, 1)
hass, context.mqtt_topic, async_handle_mqtt_message, 1)
return True
def _parse_topic(topic):
"""Parse an MQTT topic owntracks/user/dev, return (user, dev) tuple.
def _parse_topic(topic, subscribe_topic):
"""Parse an MQTT topic {sub_topic}/user/dev, return (user, dev) tuple.
Async friendly.
"""
subscription = subscribe_topic.split('/')
try:
_, user, device, *_ = topic.split('/', 3)
user_index = subscription.index('#')
except ValueError:
_LOGGER.error("Can't parse subscription topic: '%s'", subscribe_topic)
raise
topic_list = topic.split('/')
try:
user, device = topic_list[user_index], topic_list[user_index + 1]
except IndexError:
_LOGGER.error("Can't parse topic: '%s'", topic)
raise
return user, device
def _parse_see_args(message):
def _parse_see_args(message, subscribe_topic):
"""Parse the OwnTracks location parameters, into the format see expects.
Async friendly.
"""
user, device = _parse_topic(message['topic'])
user, device = _parse_topic(message['topic'], subscribe_topic)
dev_id = slugify('{}_{}'.format(user, device))
kwargs = {
'dev_id': dev_id,
@@ -185,16 +201,20 @@ def context_from_config(async_see, config):
waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
secret = config.get(CONF_SECRET)
region_mapping = config.get(CONF_REGION_MAPPING)
events_only = config.get(CONF_EVENTS_ONLY)
mqtt_topic = config.get(CONF_MQTT_TOPIC)
return OwnTracksContext(async_see, secret, max_gps_accuracy,
waypoint_import, waypoint_whitelist)
waypoint_import, waypoint_whitelist,
region_mapping, events_only, mqtt_topic)
class OwnTracksContext:
"""Hold the current OwnTracks context."""
def __init__(self, async_see, secret, max_gps_accuracy, import_waypoints,
waypoint_whitelist):
waypoint_whitelist, region_mapping, events_only, mqtt_topic):
"""Initialize an OwnTracks context."""
self.async_see = async_see
self.secret = secret
@@ -203,6 +223,9 @@ class OwnTracksContext:
self.regions_entered = defaultdict(list)
self.import_waypoints = import_waypoints
self.waypoint_whitelist = waypoint_whitelist
self.region_mapping = region_mapping
self.events_only = events_only
self.mqtt_topic = mqtt_topic
@callback
def async_valid_accuracy(self, message):
@@ -267,7 +290,11 @@ def async_handle_location_message(hass, context, message):
if not context.async_valid_accuracy(message):
return
dev_id, kwargs = _parse_see_args(message)
if context.events_only:
_LOGGER.debug("Location update ignored due to events_only setting")
return
dev_id, kwargs = _parse_see_args(message, context.mqtt_topic)
if context.regions_entered[dev_id]:
_LOGGER.debug(
@@ -283,7 +310,7 @@ def async_handle_location_message(hass, context, message):
def _async_transition_message_enter(hass, context, message, location):
"""Execute enter event."""
zone = hass.states.get("zone.{}".format(slugify(location)))
dev_id, kwargs = _parse_see_args(message)
dev_id, kwargs = _parse_see_args(message, context.mqtt_topic)
if zone is None and message.get('t') == 'b':
# Not a HA zone, and a beacon so mobile beacon.
@@ -309,7 +336,7 @@ def _async_transition_message_enter(hass, context, message, location):
@asyncio.coroutine
def _async_transition_message_leave(hass, context, message, location):
"""Execute leave event."""
dev_id, kwargs = _parse_see_args(message)
dev_id, kwargs = _parse_see_args(message, context.mqtt_topic)
regions = context.regions_entered[dev_id]
if location in regions:
@@ -352,6 +379,12 @@ def async_handle_transition_message(hass, context, message):
# OwnTracks uses - at the start of a beacon zone
# to switch on 'hold mode' - ignore this
location = message['desc'].lstrip("-")
# Create a layer of indirection for Owntracks instances that may name
# regions differently than their HA names
if location in context.region_mapping:
location = context.region_mapping[location]
if location.lower() == 'home':
location = STATE_HOME
@@ -398,7 +431,7 @@ def async_handle_waypoints_message(hass, context, message):
return
if context.waypoint_whitelist is not None:
user = _parse_topic(message['topic'])[0]
user = _parse_topic(message['topic'], context.mqtt_topic)[0]
if user not in context.waypoint_whitelist:
return
@@ -410,7 +443,7 @@ def async_handle_waypoints_message(hass, context, message):
_LOGGER.info("Got %d waypoints from %s", len(wayps), message['topic'])
name_base = ' '.join(_parse_topic(message['topic']))
name_base = ' '.join(_parse_topic(message['topic'], context.mqtt_topic))
for wayp in wayps:
yield from async_handle_waypoint(hass, name_base, wayp)
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.owntracks_http/
"""
import asyncio
import re
from aiohttp.web_exceptions import HTTPInternalServerError
@@ -43,8 +44,11 @@ class OwnTracksView(HomeAssistantView):
"""Handle an OwnTracks message."""
hass = request.app['hass']
subscription = self.context.mqtt_topic
topic = re.sub('/#$', '', subscription)
message = yield from request.json()
message['topic'] = 'owntracks/{}/{}'.format(user, device)
message['topic'] = '{}/{}/{}'.format(topic, user, device)
try:
yield from async_handle_message(hass, self.context, message)
+17 -12
View File
@@ -13,8 +13,8 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, DEFAULT_SCAN_INTERVAL, SOURCE_TYPE_ROUTER)
from homeassistant.helpers.event import track_point_in_utc_time
PLATFORM_SCHEMA, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
SOURCE_TYPE_ROUTER)
from homeassistant import util
from homeassistant import const
@@ -70,16 +70,21 @@ def setup_scanner(hass, config, see, discovery_info=None):
"""Set up the Host objects and return the update function."""
hosts = [Host(ip, dev_id, hass, config) for (dev_id, ip) in
config[const.CONF_HOSTS].items()]
interval = timedelta(seconds=len(hosts) * config[CONF_PING_COUNT]) + \
DEFAULT_SCAN_INTERVAL
_LOGGER.info("Started ping tracker with interval=%s on hosts: %s",
interval, ",".join([host.ip_address for host in hosts]))
interval = config.get(CONF_SCAN_INTERVAL,
timedelta(seconds=len(hosts) *
config[CONF_PING_COUNT])
+ DEFAULT_SCAN_INTERVAL)
_LOGGER.debug("Started ping tracker with interval=%s on hosts: %s",
interval, ",".join([host.ip_address for host in hosts]))
def update(now):
def update_interval(now):
"""Update all the hosts on every interval time."""
for host in hosts:
host.update(see)
track_point_in_utc_time(hass, update, util.dt.utcnow() + interval)
return True
try:
for host in hosts:
host.update(see)
finally:
hass.helpers.event.track_point_in_utc_time(
update_interval, util.dt.utcnow() + interval)
return update(util.dt.utcnow())
update_interval(None)
return True
@@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
REQUIREMENTS = ['pysnmp==4.4.2']
REQUIREMENTS = ['pysnmp==4.4.4']
_LOGGER = logging.getLogger(__name__)
+18 -20
View File
@@ -19,7 +19,7 @@ from homeassistant.util.json import load_json, save_json
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pytile==1.0.0']
REQUIREMENTS = ['pytile==1.1.0']
CLIENT_UUID_CONFIG_FILE = '.tile.conf'
DEFAULT_ICON = 'mdi:bluetooth'
@@ -29,14 +29,15 @@ ATTR_ALTITUDE = 'altitude'
ATTR_CONNECTION_STATE = 'connection_state'
ATTR_IS_DEAD = 'is_dead'
ATTR_IS_LOST = 'is_lost'
ATTR_LAST_SEEN = 'last_seen'
ATTR_LAST_UPDATED = 'last_updated'
ATTR_RING_STATE = 'ring_state'
ATTR_VOIP_STATE = 'voip_state'
CONF_SHOW_INACTIVE = 'show_inactive'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SHOW_INACTIVE, default=False): cv.boolean,
vol.Optional(CONF_MONITORED_VARIABLES):
vol.All(cv.ensure_list, [vol.In(DEVICE_TYPES)]),
})
@@ -79,6 +80,7 @@ class TileDeviceScanner(DeviceScanner):
_LOGGER.debug('Client UUID: %s', self._client.client_uuid)
_LOGGER.debug('User UUID: %s', self._client.user_uuid)
self._show_inactive = config.get(CONF_SHOW_INACTIVE)
self._types = config.get(CONF_MONITORED_VARIABLES)
self.devices = {}
@@ -91,29 +93,25 @@ class TileDeviceScanner(DeviceScanner):
def _update_info(self, now=None) -> None:
"""Update the device info."""
device_data = self._client.get_tiles(type_whitelist=self._types)
self.devices = self._client.get_tiles(
type_whitelist=self._types, show_inactive=self._show_inactive)
try:
self.devices = device_data['result']
except KeyError:
if not self.devices:
_LOGGER.warning('No Tiles found')
_LOGGER.debug(device_data)
return
for info in self.devices.values():
dev_id = 'tile_{0}'.format(slugify(info['name']))
lat = info['tileState']['latitude']
lon = info['tileState']['longitude']
for dev in self.devices:
dev_id = 'tile_{0}'.format(slugify(dev['name']))
lat = dev['tileState']['latitude']
lon = dev['tileState']['longitude']
attrs = {
ATTR_ALTITUDE: info['tileState']['altitude'],
ATTR_CONNECTION_STATE: info['tileState']['connection_state'],
ATTR_IS_DEAD: info['is_dead'],
ATTR_IS_LOST: info['tileState']['is_lost'],
ATTR_LAST_SEEN: info['tileState']['timestamp'],
ATTR_LAST_UPDATED: device_data['timestamp_ms'],
ATTR_RING_STATE: info['tileState']['ring_state'],
ATTR_VOIP_STATE: info['tileState']['voip_state'],
ATTR_ALTITUDE: dev['tileState']['altitude'],
ATTR_CONNECTION_STATE: dev['tileState']['connection_state'],
ATTR_IS_DEAD: dev['is_dead'],
ATTR_IS_LOST: dev['tileState']['is_lost'],
ATTR_RING_STATE: dev['tileState']['ring_state'],
ATTR_VOIP_STATE: dev['tileState']['voip_state'],
}
self.see(
View File
+61 -43
View File
@@ -9,7 +9,7 @@ import logging
import voluptuous as vol
from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import intent, template
from homeassistant.components.http import HomeAssistantView
@@ -33,6 +33,10 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
class DialogFlowError(HomeAssistantError):
"""Raised when a DialogFlow error happens."""
@asyncio.coroutine
def async_setup(hass, config):
"""Set up Dialogflow component."""
@@ -51,57 +55,71 @@ class DialogflowIntentsView(HomeAssistantView):
def post(self, request):
"""Handle Dialogflow."""
hass = request.app['hass']
data = yield from request.json()
message = yield from request.json()
_LOGGER.debug("Received Dialogflow request: %s", data)
req = data.get('result')
if req is None:
_LOGGER.error("Received invalid data from Dialogflow: %s", data)
return self.json_message(
"Expected result value not received", HTTP_BAD_REQUEST)
action_incomplete = req['actionIncomplete']
if action_incomplete:
return None
action = req.get('action')
parameters = req.get('parameters')
dialogflow_response = DialogflowResponse(parameters)
if action == "":
_LOGGER.warning("Received intent with empty action")
dialogflow_response.add_speech(
"You have not defined an action in your Dialogflow intent.")
return self.json(dialogflow_response)
_LOGGER.debug("Received Dialogflow request: %s", message)
try:
intent_response = yield from intent.async_handle(
hass, DOMAIN, action,
{key: {'value': value} for key, value
in parameters.items()})
response = yield from async_handle_message(hass, message)
return b'' if response is None else self.json(response)
except DialogFlowError as err:
_LOGGER.warning(str(err))
return self.json(dialogflow_error_response(
hass, message, str(err)))
except intent.UnknownIntent as err:
_LOGGER.warning("Received unknown intent %s", action)
dialogflow_response.add_speech(
"This intent is not yet configured within Home Assistant.")
return self.json(dialogflow_response)
_LOGGER.warning(str(err))
return self.json(dialogflow_error_response(
hass, message,
"This intent is not yet configured within Home Assistant."))
except intent.InvalidSlotInfo as err:
_LOGGER.error("Received invalid slot data: %s", err)
return self.json_message('Invalid slot data received',
HTTP_BAD_REQUEST)
except intent.IntentError:
_LOGGER.exception("Error handling request for %s", action)
return self.json_message('Error handling intent', HTTP_BAD_REQUEST)
_LOGGER.warning(str(err))
return self.json(dialogflow_error_response(
hass, message,
"Invalid slot information received for this intent."))
if 'plain' in intent_response.speech:
dialogflow_response.add_speech(
intent_response.speech['plain']['speech'])
except intent.IntentError as err:
_LOGGER.warning(str(err))
return self.json(dialogflow_error_response(
hass, message, "Error handling intent."))
return self.json(dialogflow_response)
def dialogflow_error_response(hass, message, error):
"""Return a response saying the error message."""
dialogflow_response = DialogflowResponse(message['result']['parameters'])
dialogflow_response.add_speech(error)
return dialogflow_response.as_dict()
@asyncio.coroutine
def async_handle_message(hass, message):
"""Handle a DialogFlow message."""
req = message.get('result')
action_incomplete = req['actionIncomplete']
if action_incomplete:
return None
action = req.get('action', '')
parameters = req.get('parameters')
dialogflow_response = DialogflowResponse(parameters)
if action == "":
raise DialogFlowError(
"You have not defined an action in your Dialogflow intent.")
intent_response = yield from intent.async_handle(
hass, DOMAIN, action,
{key: {'value': value} for key, value
in parameters.items()})
if 'plain' in intent_response.speech:
dialogflow_response.add_speech(
intent_response.speech['plain']['speech'])
return dialogflow_response.as_dict()
class DialogflowResponse(object):
+9 -3
View File
@@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-digitalocean==1.12']
REQUIREMENTS = ['python-digitalocean==1.13.2']
_LOGGER = logging.getLogger(__name__)
@@ -44,13 +44,19 @@ CONFIG_SCHEMA = vol.Schema({
def setup(hass, config):
"""Set up the Digital Ocean component."""
import digitalocean
conf = config[DOMAIN]
access_token = conf.get(CONF_ACCESS_TOKEN)
digital = DigitalOcean(access_token)
if not digital.manager.get_account():
_LOGGER.error("No Digital Ocean account found for the given API Token")
try:
if not digital.manager.get_account():
_LOGGER.error("No account found for the given API token")
return False
except digitalocean.baseapi.DataReadError:
_LOGGER.error("API token not valid for authentication")
return False
hass.data[DATA_DIGITAL_OCEAN] = digital
+3
View File
@@ -37,6 +37,8 @@ SERVICE_WINK = 'wink'
SERVICE_XIAOMI_GW = 'xiaomi_gw'
SERVICE_TELLDUSLIVE = 'tellstick'
SERVICE_HUE = 'philips_hue'
SERVICE_DECONZ = 'deconz'
SERVICE_DAIKIN = 'daikin'
SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None),
@@ -50,6 +52,7 @@ SERVICE_HANDLERS = {
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None),
SERVICE_HUE: ('hue', None),
SERVICE_DECONZ: ('deconz', None),
'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'),
'plex_mediaserver': ('media_player', 'plex'),
+47 -5
View File
@@ -1,40 +1,54 @@
"""Support for a DoorBird video doorbell."""
"""
Support for DoorBird device.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/doorbird/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.components.http import HomeAssistantView
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['DoorBirdPy==0.1.0']
REQUIREMENTS = ['DoorBirdPy==0.1.2']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'doorbird'
API_URL = '/api/{}'.format(DOMAIN)
CONF_DOORBELL_EVENTS = 'doorbell_events'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_DOORBELL_EVENTS): cv.boolean,
})
}, extra=vol.ALLOW_EXTRA)
SENSOR_DOORBELL = 'doorbell'
def setup(hass, config):
"""Set up the DoorBird component."""
from doorbirdpy import DoorBird
device_ip = config[DOMAIN].get(CONF_HOST)
username = config[DOMAIN].get(CONF_USERNAME)
password = config[DOMAIN].get(CONF_PASSWORD)
from doorbirdpy import DoorBird
device = DoorBird(device_ip, username, password)
status = device.ready()
if status[0]:
_LOGGER.info("Connected to DoorBird at %s as %s", device_ip, username)
hass.data[DOMAIN] = device
return True
elif status[1] == 401:
_LOGGER.error("Authorization rejected by DoorBird at %s", device_ip)
return False
@@ -42,3 +56,31 @@ def setup(hass, config):
_LOGGER.error("Could not connect to DoorBird at %s: Error %s",
device_ip, str(status[1]))
return False
if config[DOMAIN].get(CONF_DOORBELL_EVENTS):
# Provide an endpoint for the device to call to trigger events
hass.http.register_view(DoorbirdRequestView())
# This will make HA the only service that gets doorbell events
url = '{}{}/{}'.format(
hass.config.api.base_url, API_URL, SENSOR_DOORBELL)
device.reset_notifications()
device.subscribe_notification(SENSOR_DOORBELL, url)
return True
class DoorbirdRequestView(HomeAssistantView):
"""Provide a page for the device to call."""
url = API_URL
name = API_URL[1:].replace('/', ':')
extra_urls = [API_URL + '/{sensor}']
# pylint: disable=no-self-use
@asyncio.coroutine
def get(self, request, sensor):
"""Respond to requests from the device."""
hass = request.app['hass']
hass.bus.async_fire('{}_{}'.format(DOMAIN, sensor))
return 'OK'
-7
View File
@@ -6,13 +6,11 @@ https://home-assistant.io/components/eight_sleep/
"""
import asyncio
import logging
import os
from datetime import timedelta
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_SENSORS, CONF_BINARY_SENSORS,
ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP)
@@ -159,10 +157,6 @@ def async_setup(hass, config):
CONF_BINARY_SENSORS: binary_sensors,
}, config))
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine
def async_service_handler(service):
"""Handle eight sleep service calls."""
@@ -183,7 +177,6 @@ def async_setup(hass, config):
# Register services
hass.services.async_register(
DOMAIN, SERVICE_HEAT_SET, async_service_handler,
descriptions[DOMAIN].get(SERVICE_HEAT_SET),
schema=SERVICE_EIGHT_SCHEMA)
@asyncio.coroutine
+1 -9
View File
@@ -8,12 +8,10 @@ import asyncio
from datetime import timedelta
import functools as ft
import logging
import os
import voluptuous as vol
from homeassistant.components import group
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TOGGLE,
SERVICE_TURN_OFF, ATTR_ENTITY_ID,
STATE_UNKNOWN)
@@ -225,16 +223,10 @@ def async_setup(hass, config: dict):
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
# Listen for fan service calls.
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
for service_name in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[service_name].get('schema')
hass.services.async_register(
DOMAIN, service_name, async_handle_fan_service,
descriptions.get(service_name), schema=schema)
DOMAIN, service_name, async_handle_fan_service, schema=schema)
return True

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