Compare commits

...

294 Commits

Author SHA1 Message Date
Paulus Schoutsen
9db15aab92 Merge pull request #16256 from home-assistant/rc
0.77
2018-08-29 12:10:44 +02:00
Paulus Schoutsen
f01e1ef0aa Version bump to 0.77.0 2018-08-29 10:29:51 +02:00
Robert Svensson
b5919ce92c def device shouldnt call it self but self._device (#16255) 2018-08-29 10:29:24 +02:00
Jason Hu
f9b1fb5906 Tweak MFA login flow (#16254)
* Tweak MFA login flow

* Fix typo
2018-08-29 10:29:24 +02:00
Paulus Schoutsen
9238261e17 Update translations 2018-08-29 10:28:44 +02:00
Paulus Schoutsen
8ec109d255 Bump frontend to 20180829.0 2018-08-29 10:28:11 +02:00
Paulus Schoutsen
2ea2bcab77 Bumped version to 0.77.0b4 2018-08-28 20:59:38 +02:00
Jason Hu
8d38016b0c Blow up startup if init auth providers or modules failed (#16240)
* Blow up startup if init auth providers or modules failed

* Delete core.entity_registry
2018-08-28 20:59:30 +02:00
Jason Hu
d994d6bfad Change log level to error when auth provider failed loading (#16235) 2018-08-28 20:59:29 +02:00
Paulus Schoutsen
573f5de148 Avoid insecure pycryptodome (#16238) 2018-08-28 20:57:46 +02:00
Paulus Schoutsen
667f9c6fe4 Package loadable: compare case insensitive (#16234) 2018-08-28 20:57:46 +02:00
Paulus Schoutsen
f708292015 Warning missed a space (#16233) 2018-08-28 20:57:45 +02:00
Paulus Schoutsen
c50a7deb92 Fix hangouts (#16232) 2018-08-28 20:57:45 +02:00
Paulus Schoutsen
11fcffda4c Update translations 2018-08-28 20:56:12 +02:00
Paulus Schoutsen
e9cc359abe Bumped version to 0.77.0b3 2018-08-28 00:38:23 +02:00
Paulus Schoutsen
9b01972b41 Update trusted networks flow (#16227)
* Update the trusted networks flow

* Fix tests

* Remove errors
2018-08-28 00:38:08 +02:00
Paulus Schoutsen
3e65009ea9 Fix device telldus (#16224) 2018-08-28 00:38:08 +02:00
Marcel Hoppe
a953601abd rewrite hangouts to use intents instead of commands (#16220)
* rewrite hangouts to use intents instead of commands

* small fixes

* remove configured_hangouts check and CONFIG_SCHEMA

* Lint

* add import from .config_flow
2018-08-28 00:38:07 +02:00
Paulus Schoutsen
2744702f9b Change auth warning (#16216) 2018-08-28 00:38:07 +02:00
Dan Klaffenbach
9c7d4381a1 homematic: Make device avilable again when UNREACH becomes False (#16202) 2018-08-28 00:38:06 +02:00
Paulus Schoutsen
914436f3d5 Bump frontend to 20180827.0 2018-08-27 22:28:37 +02:00
Paulus Schoutsen
adb5579690 Update translations 2018-08-27 10:17:10 +02:00
Paulus Schoutsen
8413101148 Bumped version to 0.77.0b2 2018-08-26 22:53:20 +02:00
Paulus Schoutsen
8fb66c351e Add new translations 2018-08-26 22:53:08 +02:00
Paulus Schoutsen
16ad9c2ae6 Update translations 2018-08-26 22:53:08 +02:00
Robert Svensson
2ad938ed44 Revert changes to platforms using self.device (#16209)
* Revert tank_utility

* Fix Soundtouch

* Fix Plex

* Fix Emby

* Fix Radiotherm

* Fix Juicenet

* Fix Qwikswitch

* Fix Xiaomi miio

* Fix Nest

* Fix Tellduslive

* Fix KNX
2018-08-26 22:51:19 +02:00
Penny Wood
969b15a297 Update aiohttp to version 3.4.0. (#16198) 2018-08-26 22:51:18 +02:00
Marcel Hoppe
c8449d8f8a remove hangouts.users state, simplifies hangouts.conversations (#16191) 2018-08-26 22:51:18 +02:00
PhracturedBlue
6992a6fe6d Handle exception from pillow (#16190) 2018-08-26 22:51:17 +02:00
Jason Hu
2ece671bfd Add Time-based Onetime Password Multi-factor Authentication Module (#16129)
* Add Time-based Onetime Password Multi-factor Auth

Add TOTP setup flow, generate QR code

* Resolve rebase issue

* Use svg instead png for QR code

* Lint and typing

* Fix translation

* Load totp auth module by default

* use <svg> tag instead markdown image

* Update strings

* Cleanup
2018-08-26 22:51:17 +02:00
Matt Hamilton
c13e5fcb92 Replace pbkdf2 with bcrypt (#16071)
* Replace pbkdf2 with bcrypt

bcrypt isn't inherently better than pbkdf2, but everything "just works"
out of the box.

  * the hash verification routine now only computes one hash per call
  * a per-user salt is built into the hash as opposed to the current
  global salt
  * bcrypt.checkpw() is immune to timing attacks regardless of input
  * hash strength is a function of real time benchmarks and a
  "difficulty" level, meaning we won't have to ever update the iteration
  count

* WIP: add hash upgrade mechanism

* WIP: clarify decode issue

* remove stale testing code

* Fix test

* Ensure incorrect legacy passwords fail

* Add better invalid legacy password test

* Lint

* Run tests in async scope
2018-08-26 22:51:16 +02:00
Paulus Schoutsen
3783d1ce90 Update frontend to 20180826.0 2018-08-26 21:30:29 +02:00
Paulus Schoutsen
9ffcd2d86a Bumped version to 0.77.0b1 2018-08-25 11:16:01 +02:00
Jason Hu
66a8bede12 Default load trusted_network auth provider if configured trusted networks (#16184) 2018-08-25 11:15:55 +02:00
Jason Hu
c2891b9905 Tweak log level for bearer token warning (#16182) 2018-08-25 11:15:54 +02:00
Paulus Schoutsen
b8c272258e Fix hangouts (#16180) 2018-08-25 11:15:54 +02:00
Nate Clark
4cb9ac72b4 fix error message for cv.matches_regex (#16175) 2018-08-25 11:15:53 +02:00
Robert Svensson
cf8bd92d4d Device registry store config entry (#16152)
* Allow device registry to optionally store config entries

* Connections and identifiers are now sets with tupels

* Make config entries mandatory

* Fix duplicate keys in test

* Rename device to device_info

* Entity platform should only create device entries if config_entry_id exists

* Fix Soundtouch tests

* Revert soundtouch to use self.device

* Fix baloobs comments

* Correct type in test
2018-08-25 11:15:53 +02:00
Nate Clark
90b2257347 Decouple Konnected entity setup from discovery (#16146)
* decouple entity setup from discovery

* validate that device_id is a full MAC address
2018-08-25 11:15:52 +02:00
Jason Hu
914d90a2bc Add multi-factor auth module setup flow (#16141)
* Add mfa setup flow

* Lint

* Address code review comment

* Fix unit test

* Add assertion for WS response ordering

* Missed a return

* Remove setup_schema from MFA base class

* Move auth.util.validate_current_user -> webscoket_api.ws_require_user
2018-08-25 11:15:52 +02:00
Robert Svensson
e567b2281d deCONZ - Support device registry (#16115)
Add support for device registry in deCONZ component
2018-08-25 11:15:51 +02:00
Paulus Schoutsen
bb6567f84c Bump frontend to 20180825.0 2018-08-25 11:15:19 +02:00
Paulus Schoutsen
4f8fec6494 Bumped version to 0.77.0b0 2018-08-24 17:03:05 +02:00
Paulus Schoutsen
57979faa9c Merge remote-tracking branch 'origin/master' into dev 2018-08-24 17:02:44 +02:00
Paulus Schoutsen
994b829cb4 add_devices -> add_entities (#16171)
* add_devices -> add_entities

* Lint

* PyLint

* Revert external method in scsgate
2018-08-24 16:37:30 +02:00
Robert Svensson
37fd438717 deCONZ - Allow sub second light transitions (#16170)
Solves https://github.com/home-assistant/home-assistant/issues/16075
2018-08-24 16:15:28 +02:00
Adam Mills
5e301dd599 Hangouts localization typo fix (#16174) 2018-08-24 16:13:58 +02:00
Paulus Schoutsen
3d5b3fb6ff Update translations 2018-08-24 15:54:47 +02:00
Paulus Schoutsen
6c5f98668e Bump frontend to 20180824.0 2018-08-24 15:54:04 +02:00
Marcel Hoppe
ef0eab0f40 Hangouts (#16049)
* add a component for hangouts

* add a notify component for hangouts

* add an extra message as title

* add support to listen to all conversations hangouts has

* move hangouts to package and add parameter documentation

* update .coveragerc and requirements_all.txt

* makes linter happy again

* bugfix

* add conversations parameter to command words

* Move the resolution of conversation names to conversations in own a function

* typo

* rename group of exclusion form 'id' to 'id or name'

* refactoring and use config_flow

* makes linter happy again

* remove unused imports

* fix not working regex commands

* fix translations

* cleanup

* remove step_init

* remove logging entry

* clean up events

* move constant

* remove unsed import

* add new files to .converagerc

* isort imports

* add hangouts_utils to ignored packages

* upadte doc and format

* fix I/O not in executor jon

* rename SERVICE_UPDATE_USERS_AND_CONVERSATIONS to SERVICE_UPDATE

* move EVENT_HANGOUTS_{CONNECTED,DISCONNECTED} to dispatcher

* add config flow tests

* Update tox.ini
2018-08-24 10:39:35 +02:00
Ville Skyttä
dd9d53c83e Update pydocstyle to 2.1.1 and flake8-docstrings to 1.3.0 (#14557)
* Update pydocstyle to 2.1.1 and flake8-docstrings to 1.3.0

* Pydocstyle D401 fixes
2018-08-24 10:28:43 +02:00
Ville Skyttä
89d856d147 Spelling fixes (#16150) 2018-08-23 22:56:18 +02:00
Paulus Schoutsen
156c6e2025 Remove commented out API password from default config (#16147) 2018-08-23 22:16:31 +02:00
Paulus Schoutsen
d21d7cef4c Enable auth by default 🙈 (#16107)
* Enable auth by default

* Only default legacy_api_password if api_password set

* Tweak bool check

* typing
2018-08-23 13:38:08 +02:00
Paulus Schoutsen
249981de96 Prevent legacy api password with empty password (#16127)
* Prevent legacy api password with empty password

* Typing
2018-08-23 12:56:01 +02:00
squidwardy
8e173f1658 Add support for JS modules in custom panels (#16096)
* Added backend support for JavaScript modules in custom panels.

* Fixed test_panel_custom.py

* Delete core.entity_registry

* Update panel_custom.py

* Corrected panel_custom.py with module_url.

* Rebase

* Missed elif

* Add vol.Exclusive module_url

* Correct vol.Exclusive usage

* Test for js module

* Corrected line continuation indentation

* Added webcomponent path to exclusive group

* Corrected line length

* Line break

* Test for conflicting url options

* Self -> hass fix

* Fix self -> hass again

* Use assert_setup_component

* Setup missing

* Correct test

* Fix again

* Fix

* Mising async

* Fix

* test real

* Test real

* Final

* check

* Final check

* safety

* Final commit and check

* Removed unused dependencies

* Test for multiple url options in config
2018-08-23 11:14:18 +02:00
Diogo Gomes
ced5eeacc2 Adds support for routers implementing IGDv2 (#16108)
* Adds support for IGDv2

* bump pyupnp_async

* bump pyupnp_async

* better debug

* fix test
2018-08-23 10:54:02 +02:00
JC Connell
4155e8a31f Add support for NOAA tide information (new PR) (#15947)
* Adding noaa-tides changes to new branch.

* Fix typo in .coverageac

* Incorporate @MartinHjelmare and @fabaff changes.

* Disable pylint error and add error message for unavailable station.

* Two spaces before inline comments

* Increment py_noaa version to 0.3.0

* Updated requirements.py

* Minor changes
2018-08-23 00:21:46 +02:00
Eduard van Valkenburg
4ad76d8916 Upgrade brunt package (#16130)
* Bumped package version to fix a bug with Python before 3.6

* update of package version in requirements_all
2018-08-22 20:12:54 +02:00
Sebastian Muszynski
478eb48e93 Fix the protocol v2 data_key of several aqara devices (#16112)
* Fix the protocol v2 data_key of several aqara devices

* Incorporate review
2018-08-22 15:29:43 +02:00
Fabian Affolter
d8ae079757 Upgrade youtube_dl to 2018.08.22 (#16125) 2018-08-22 13:55:48 +02:00
Fabian Affolter
b0c2d24997 Upgrade numpy to 1.15.1 (#16126) 2018-08-22 13:55:11 +02:00
Paulus Schoutsen
2e6cb2235c Check correctly if package is loadable (#16121) 2018-08-22 12:17:14 +02:00
Robert Svensson
0009be595c Device Registry (#15980)
* First draft

* Generate device id

* No obscure registry

* Dont store config_entry_id in device

* Storage

* Small mistake on rebase

* Do storage more like entity registry

* Improve device identification

* Add tests

* Remove deconz device support from PR

* Fix hound comments, voff!

* Fix comments and clean up

* Fix proper indentation

* Fix pydoc issues

* Fix mochad component to not use self.device

* Fix mochad light platform to not use self.device

* Fix TankUtilitySensor to not use self.device

* Fix Soundtouch to not use self.device

* Fix Plex to not use self.device

* Fix Emby to not use self.device

* Fix Heatmiser to not use self.device

* Fix Wemo lights to not use self.device

* Fix Lifx to not use self.device

* Fix Radiotherm to not use self.device

* Fix Juicenet to not use self.device

* Fix Qwikswitch to not use self.device

* Fix Xiaomi miio to not use self.device

* Fix Nest to not use self.device

* Fix Tellduslive to not use self.device

* Fix Knx to not use self.device

* Clean up a small mistake in soundtouch

* Fix comment from Ballob

* Fix bad indentation

* Fix indentatin

* Lint

* Remove unused variable

* Lint
2018-08-22 10:46:37 +02:00
Jason Hu
7e7f9bc6ac Add multi-factor authentication modules (#15489)
* Get user after login flow finished

* Add multi factor authentication support

* Typings
2018-08-22 09:52:34 +02:00
Jerad Meisner
ae63980152 Remove unit_of_measurement from climate entities (#16012)
* Remove unit_of_measurement from climate base class.

* Updated google_assistant component and tests to use core temp units.

* Fixes

* Convert Alexa component to use core temp units for climate entities.

* Fix tests.

* Converted prometheus component.

* Remove unit_of_measurement from homekit thermostat tests.

* Small fix.
2018-08-22 09:17:29 +02:00
Tom Harris
a31501d99e Merge insteon_plm and insteon_local to insteon component (#16102)
* Implement X10

* Add X10 after add_device_callback

* Ref device by id not hex and add x10OnOffSwitch name

* X10 services and add sensor device

* Correctly reference X10_HOUSECODE_SCHEMA

* Log adding of X10 devices

* Add X10 All Units Off, All Lights On and All Lights Off devices

* Correct ref to X10 states vs devices

* Add X10 All Units Off, All Lights On and All Lights Off devices

* Correct X10 config

* Debug x10 device additions

* Config x10 from bool to housecode char

* Pass PLM to X10 device create

* Remove PLM to call to add_x10_device

* Unconfuse x10 config and method names

* Correct spelling of x10_all_lights_off_housecode

* Bump insteonplm to 0.10.0 to support X10

* Add host to config options

* Add username and password to config for hub connectivity

* Add username and password to config for hub

* Convert port to int if host is defined

* Add KeypadLinc

* Update config schema to require either port or host

* Solidify Hub and PLM configuration to ensure proper settings

* Update hub schema

* Bump insteonplm version

* Fix pylint and flake issues

* Bump insteonplm to 0.12.1

* Merge insteon_plm and insteon_local to insteon

* Rename insteon_plm to insteon

* Bump insteonplm to 0.12.2

* Flake8 cleanup

* Update .coveragerc for insteon_plm, insteon_local and insteon changes

* Add persistent notification

* Fix reference to insteon_plm

* Fix indentation

* Shorten message and fix grammer

* Add comment to remove in release 0.90

* Hound fix
2018-08-22 09:09:04 +02:00
Fabian Affolter
6864a44b5a Upgrade sendgrid to 5.6.0 (#16111) 2018-08-22 07:16:21 +02:00
Fabian Affolter
523af4fbca Upgrade shodan to 1.9.1 (#16113) 2018-08-22 07:15:56 +02:00
Dan Klaffenbach
b9733d0d99 homematic: Add homematic.put_paramset service (#16024)
Service to call putParamset method of XML-RPC API
2018-08-22 00:20:26 +02:00
Sebastian Muszynski
7ed8ed83e3 Bump python-miio version (#16110) 2018-08-21 21:25:48 +02:00
Fabian Affolter
0e1fb74e1b Minor updates (#16106) 2018-08-21 21:25:16 +02:00
Jason Hu
1ce51bfbd6 Refactoring login flow (#16104)
* Abstract LoginFlow

* Lint and typings
2018-08-21 11:03:38 -07:00
Paulus Schoutsen
cdb8361050 Add support for revoking refresh tokens (#16095)
* Add support for revoking refresh tokens

* Lint

* Split revoke logic in own method

* Simplify

* Update docs
2018-08-21 11:02:55 -07:00
Jason Hu
00c6f56cc8 Allow finish_flow callback to change data entry result type (#16100)
* Allow finish_flow callback to change data entry result type

* Add unit test
2018-08-21 10:48:24 -07:00
Paulus Schoutsen
b26506ad4a Use new session when fetching remote urls (#16093) 2018-08-21 19:03:46 +02:00
Paulus Schoutsen
7bb5344942 Remove homeassistant.remote (#16099)
* Remove homeassistant.remote

* Use direct import for API

* Fix docstring
2018-08-21 15:49:58 +02:00
Krasimir Zhelev
ae5c4c7e13 Upgrade afsapi to 0.0.4, prevents aiohttp session close message, Fixes #13099 (#16098) 2018-08-21 15:30:40 +02:00
Flip Hess
507e8f8f12 Add verify ssl to generic camera (#15949)
* Add verify_ssl option to generic camera

* Remove flake8 errors

* Add test for ssl verification on and off

* Fix lint errors
2018-08-21 15:29:11 +02:00
Paulus Schoutsen
5a15b2c036 Merge pull request #16094 from home-assistant/rc
0.76.2
2018-08-21 12:20:06 +02:00
Paulus Schoutsen
977d86e7ca Bumped version to 0.76.2 2018-08-21 11:43:14 +02:00
Paulus Schoutsen
bd776c84bc Forgiving add index in migration (#16092) 2018-08-21 11:43:08 +02:00
Paulus Schoutsen
68cd65567d Forgiving add index in migration (#16092) 2018-08-21 11:41:52 +02:00
Ville Skyttä
ef07460792 Upgrade pytest to 3.7.2 (#16091) 2018-08-21 10:56:28 +02:00
Jason Hu
f84a31871e Get user after login flow finished (#16047)
* Get user after login flow finished

* Add optional parameter 'type' to /auth/login_flow

* Update __init__.py
2018-08-21 10:18:04 +02:00
Daniel Perna
b1ba11510b Update pyhomematic to 0.1.47 (#16083) 2018-08-20 23:43:04 +02:00
Daniel Høyer Iversen
439f7978c3 fritzdect change to current_power_w (#16079) 2018-08-20 22:42:48 +02:00
Andrey Kupreychik
85a724e289 Bumped NDMS2 client library to 0.0.4 to get compatible with python 3.5 (#16077) 2018-08-20 18:51:25 +02:00
Greg Laabs
df6239e0fc Add ecovacs component (#15520)
* Ecovacs Deebot vacuums

* All core features implemented

Getting fan speed and locating the vac are still unsupported until sucks adds support

* Move init queries to the added_to_hass method

* Adding support for subscribing to events from the sucks library

This support does not exist in sucks yet; this commit serves as a sort of TDD approach of what such support COULD look like.

* Add OverloadUT as ecovacs code owner

* Full support for Ecovacs vacuums (Deebot)

* Add requirements

* Linting fixes

* Make API Device ID random on each boot

* Fix unique ID

Never worked before, as it should have been looking for a key, not an attribute

* Fix random string generation to work in Python 3.5 (thanks, Travis!)

* Add new files to .coveragerc

* Code review changes

(Will require a sucks version bump in a coming commit; waiting for it to release)

* Bump sucks to 0.9.1 now that it has released

* Update requirements_all.txt as well

* Bump sucks version to fix lifespan value errors

* Revert to sucks 0.9.1 and include a fix for a bug in that release

Sucks is being slow to release currently, so doing this so we can get a version out the door.

* Switch state_attributes to device_state_attributes
2018-08-20 17:42:53 +02:00
Paulus Schoutsen
1be61df9c0 Add recent context (#15989)
* Add recent context

* Add async_set_context to components not using new services
2018-08-20 17:39:53 +02:00
Paulus Schoutsen
d1e1b9b38a Deprecated stuff (#16019)
* Use async with for locks

* Fix regex in template test

* Close session correctly

* Use correct current_task method

* push camera cleanup

* Lint

* Revert current_task

* Update websocket_api.py

* Mock executor_job betteR

* Fix async_create_task mock
2018-08-20 16:34:18 +02:00
Tim Bailey
975befd136 TpLink Device Tracker Error (#15918)
* Fix 'Error setting up platform tplink' error when a scanner fails even if another scanner would succeed

* Try to fix tox errors

* Adjust code based on PR comments
2018-08-20 14:34:52 +02:00
Oleksii Serdiuk
a708a81fa8 openuv: Add Current UV Level to list of conditions (#16042)
Calculated from UV index, based on table from
https://www.openuv.io/kb/uv-index-levels-colors
2018-08-20 14:22:41 +02:00
Paulus Schoutsen
18d19fde0b Alexa: context + log events (#16023) 2018-08-20 14:18:07 +02:00
Kevin Siml
1f0d113688 Update pushsafer.py (#16060) 2018-08-20 14:11:52 +02:00
Paulus Schoutsen
121abb450a Use aiohttp web.AppRunner (#16020)
* Use aiohttp web.AppRunner

* Stop site
2018-08-20 14:03:35 +02:00
Paulus Schoutsen
1be388c587 Update frontend to 20180820.0 2018-08-20 11:52:47 +02:00
Paulus Schoutsen
994e2b6624 Update frontend to 20180820.0 2018-08-20 11:52:36 +02:00
Ville Skyttä
dbd0763f83 Grammar and spelling fixes (#16065) 2018-08-19 22:29:08 +02:00
Paulus Schoutsen
e1b2e00cf6 Merge pull request #16064 from home-assistant/rc
0.76.1
2018-08-19 20:07:44 +02:00
Paulus Schoutsen
3c5e62d47e Column syntax fix + Add a file if migration in progress (#16061)
* Add a file if migration in progress

* Warning

* Convert message for migration to warning
2018-08-19 19:01:11 +02:00
Paulus Schoutsen
3be301fac9 Bumped version to 0.76.1 2018-08-19 18:58:21 +02:00
Paulus Schoutsen
9c3251b5f0 Add notify platforms to loaded components (#16063) 2018-08-19 18:58:13 +02:00
huangyupeng
cb44607e96 Tuya fix login problem and add login platform param (#16058)
* add a platform param to distinguish different app's account.

* fix requirements
2018-08-19 18:58:12 +02:00
Paulus Schoutsen
1c8ef4e196 Add forgiving add column (#16057)
* Add forgiving add column

* Lint
2018-08-19 18:58:12 +02:00
Paulus Schoutsen
9e1fa7ef42 Column syntax fix + Add a file if migration in progress (#16061)
* Add a file if migration in progress

* Warning

* Convert message for migration to warning
2018-08-19 18:57:06 +02:00
Paulus Schoutsen
7c95e96ce8 Add notify platforms to loaded components (#16063) 2018-08-19 18:56:31 +02:00
huangyupeng
21b88f2fe8 Tuya fix login problem and add login platform param (#16058)
* add a platform param to distinguish different app's account.

* fix requirements
2018-08-19 18:55:10 +02:00
Paulus Schoutsen
81d3161a5e Add forgiving add column (#16057)
* Add forgiving add column

* Lint
2018-08-19 17:22:09 +02:00
Dan Klaffenbach
8beb349e88 vacuum/xiaomi_miio: Expose "sensor_dirty_left" attribute (#16003) 2018-08-19 13:57:28 +02:00
Daniel Shokouhi
c105045dab Update neato to support new StateVacuumDevice (#16035)
* Update neato to support new vacuum states

* Remove changes submitted in error

* Lint

* Review comments and fix resume cleaning

* Lint
2018-08-18 20:20:32 +02:00
Paulus Schoutsen
b901a26c47 Attempt to fix flaky TTS test (#16025) 2018-08-18 13:35:51 +02:00
Paulus Schoutsen
8ec550d6e0 Storage entity registry (#16018)
* Split out storage delayed write

* Update code using delayed save

* Fix tests

* Fix typing test

* Add callback decorator

* Migrate entity registry to storage helper

* Make double loading protection easier

* Lint

* Fix tests

* Ordered Dict
2018-08-18 13:34:33 +02:00
Paulus Schoutsen
6827256586 Bump frontend to 20180818.0 2018-08-18 11:15:46 +02:00
Paulus Schoutsen
ef193b0f64 Bump frontend to 20180818.0 2018-08-18 11:15:33 +02:00
Ed Marshall
e782e2c0f3 Handle missing mpd capabilities (#15945)
* Handle missing mpd capabilities

It is possible to configure mpd without volume or playlist support.
Gracefully degrade when either of these features appears to be missing.

Resolves: #14459, #15927

* Use longer name for exception

* Only return support flags post-connection

* Small consistency fixes to mpd.py for review.
2018-08-18 09:54:23 +02:00
Wim Haanstra
ec2e94425e Update RitAssist to support maximum speed and current address (#16037)
Update RitAssist dependency to 0.9.2 so we support fetching the current maximum speed and address for a device.
2018-08-18 09:40:29 +02:00
Diogo Gomes
9f0adc16ad Merge pull request #16031 from StevenLooman/dev
Upgrade to async_upnp_client==0.12.4
2018-08-17 22:36:03 +01:00
Steven Looman
fa88d918b1 Upgrade to async_upnp_client==0.12.4 2018-08-17 21:29:31 +02:00
Ville Skyttä
3800f00564 Disable assuming Optional type for values with None default (#16029)
https://www.python.org/dev/peps/pep-0484/#union-types
"Type checkers should move towards requiring the optional type to be
made explicit."
2018-08-17 20:22:49 +02:00
Paulus Schoutsen
2ad0bd4036 Split out storage delay save (#16017)
* Split out storage delayed write

* Update code using delayed save

* Fix tests

* Fix typing test

* Add callback decorator
2018-08-17 20:18:21 +02:00
Paulus Schoutsen
70412fc0ba Merge pull request #16027 from home-assistant/rc
0.76.0
2018-08-17 18:41:40 +02:00
Paulus Schoutsen
e4425e6a37 Version 0.76.0 2018-08-17 17:23:20 +02:00
Fabian Affolter
fdbab3e20c Upgrade sendgrid to 5.5.0 (#16021) 2018-08-17 16:39:41 +02:00
Anders Melchiorsen
f2e399ccf3 Update SoCo to 0.16 (#16007) 2018-08-17 07:41:56 +02:00
Martin Hjelmare
07840f5397 Fix check config packages key error (#15840)
* Fix packages deletion in check_config script

* The config key for packages is not present if core config validation
  failed. We need to do a safe dict deletion using dict.pop.

* Add check_config test for bad core config
2018-08-17 05:28:00 +02:00
Paulus Schoutsen
c09e7e620f Bumped version to 0.76.0b5 2018-08-16 23:02:34 +02:00
Paulus Schoutsen
92e26495da Disable the DLNA component discovery (#16006) 2018-08-16 23:02:26 +02:00
Steven Looman
061859cc4d Fix message "Updating dlna_dmr media_player took longer than ..." (#16005) 2018-08-16 23:02:26 +02:00
Steven Looman
45452e510c Fix message "Updating dlna_dmr media_player took longer than ..." (#16005) 2018-08-16 22:42:11 +02:00
Paulus Schoutsen
279ead2085 Disable the DLNA component discovery (#16006) 2018-08-16 22:41:44 +02:00
Ville Skyttä
649f17fe47 Add type hints to homeassistant.auth (#15853)
* Always load users in auth store before use

* Use namedtuple instead of dict for user meta

* Ignore auth store tokens with invalid created_at

* Add type hints to homeassistant.auth
2018-08-16 22:25:41 +02:00
Alexxander0
e9e5bce10c BMW Connected drive: option to disable the services (#15993)
* Update __init__.py

* Update bmw_connected_drive.py

* Update __init__.py

* Update bmw_connected_drive.py

* Update __init__.py

* Update __init__.py

* Update __init__.py

* Update __init__.py

* Update __init__.py

* Update __init__.py

* Update __init__.py

* Update bmw_connected_drive.py
2018-08-16 22:19:29 +02:00
Paulus Schoutsen
e41ce1d6ec Bump frontend to 20180816.1 2018-08-16 22:18:10 +02:00
Paulus Schoutsen
bc21a1b944 Bump frontend to 20180816.1 2018-08-16 22:17:55 +02:00
Paulus Schoutsen
834077190f Clean up input-datetime (#16000) 2018-08-16 22:17:14 +02:00
Max Prokhorov
2a210607d3 Wemo custom ports and network errors handling (#14516)
* Update wemo component

* Support custom ports in static addresses
* Handle device_from_description exceptions
* Process static addresses before doing discovery
* Fail on inaccessable static address
* str.format instead of old formatting

* Validate static host[:port] earlier

* Fix comment formatting

* slice looks ambiguous in the log, keep voluptuous exception path intact
2018-08-16 08:14:54 -06:00
Paulus Schoutsen
1ff1639cef More entity service (#15998)
* Camera use entity service

* Convert climate services

* Convert light

* Convert media player

* Migrate fan
2018-08-16 14:28:59 +02:00
Paulus Schoutsen
5eccfc2604 Bumped version to 0.76.0b4 2018-08-16 14:26:52 +02:00
Paulus Schoutsen
2469bc7e2e Fix Nest async from sync (#15997) 2018-08-16 14:26:44 +02:00
Martin Hjelmare
d540a084dd Fix mysensors connection task blocking setup (#15938)
* Fix mysensors connection task blocking setup

* Schedule the connection task without having the core track the task
  to avoid blocking setup.
* Cancel the connection task, if not cancelled already, when
  home assistant stops.

* Use done instead of cancelled
2018-08-16 14:26:44 +02:00
Paulus Schoutsen
11eb29f520 Bump frontend to 20180816.0 2018-08-16 14:22:01 +02:00
Paulus Schoutsen
e4d41fe313 Bump frontend to 20180816.0 2018-08-16 14:21:49 +02:00
Martin Hjelmare
b5e7414be2 Fix mysensors connection task blocking setup (#15938)
* Fix mysensors connection task blocking setup

* Schedule the connection task without having the core track the task
  to avoid blocking setup.
* Cancel the connection task, if not cancelled already, when
  home assistant stops.

* Use done instead of cancelled
2018-08-16 14:19:42 +02:00
Paulus Schoutsen
83b0ef4e26 Fix Nest async from sync (#15997) 2018-08-16 13:46:43 +02:00
Paulus Schoutsen
b682e48e12 Entity service (#15991)
* Add entity service helper

* Use entity service helper

* Context
2018-08-16 09:50:11 +02:00
Josh Shoemaker
e52ba87af1 Upgrade aladdin_connect to 0.3 and provide Unique ID (#15986)
* Upgrade aladdin_connect to 0.2 and set unique_id

* update code to be Python 3.5 compatible
2018-08-16 07:18:29 +02:00
Paulus Schoutsen
6da0ae4d23 Bumped version to 0.76.0b3 2018-08-15 10:55:03 +02:00
Jason Hu
f8051a5698 Fix 0.76 beta2 hassio token issue (#15987) 2018-08-15 10:53:39 +02:00
Jason Hu
2306d14b5d Teak mqtt error message for 0.76 release (#15983) 2018-08-15 10:53:38 +02:00
Paulus Schoutsen
4035880003 Update translations 2018-08-15 10:52:27 +02:00
Paulus Schoutsen
9cfbd067d3 Update translations 2018-08-15 10:52:06 +02:00
Fabian Affolter
39fd70231f Upgrade psutil to 5.4.7 (#15982) 2018-08-15 10:47:58 +02:00
Jason Hu
dc460f4d6a Fix 0.76 beta2 hassio token issue (#15987) 2018-08-15 09:56:05 +02:00
Jason Hu
c31035d348 Teak mqtt error message for 0.76 release (#15983) 2018-08-15 08:09:19 +02:00
Fabian Affolter
555184a4b7 Update Glances sensor (#15981)
* Refactor Glances sensor

* Add glances_api to requirements_all.txt

* Add support for version as configuration option
2018-08-15 07:49:34 +02:00
Paulus Schoutsen
e64e84ad7a Bumped version to 0.76.0b2 2018-08-14 22:06:57 +02:00
Paulus Schoutsen
1777270aa2 Pin crypto (#15978)
* Pin crypto

* Fix PyJWT import once
2018-08-14 22:06:44 +02:00
Paulus Schoutsen
f5df567d09 Use JWT for access tokens (#15972)
* Use JWT for access tokens

* Update requirements

* Improvements
2018-08-14 22:06:44 +02:00
Paulus Schoutsen
899c2057b7 Switch to intermediate Mozilla cert profile (#15957)
* Allow choosing intermediate SSL profile

* Fix tests
2018-08-14 22:06:43 +02:00
Jason Hu
1b384c322a Remove remote.API from core.Config (#15951)
* Use core.ApiConfig replace remote.API in core.Config

* Move ApiConfig to http
2018-08-14 22:06:43 +02:00
Daniel Bowman
f4e84fbf84 remove-phantomjs-from-docker (#15936) 2018-08-14 22:06:42 +02:00
Khalid
d393380122 Fix issue when reading worxlandroid pin code (#15930)
Fixes #14050
2018-08-14 22:06:41 +02:00
Jason Hu
d0e4c95bbc MQTT embedded broker has to set its own password. (#15929) 2018-08-14 22:06:41 +02:00
Jason Hu
34e1f1b6da Add context to login flow (#15914)
* Add context to login flow

* source -> context

* Fix unit test

* Update comment
2018-08-14 22:06:40 +02:00
kbickar
6d432d19fe Added error handling for sense API timeouts (#15789)
* Added error handling for sense API timeouts

* Moved imports in function

* Moved imports to more appropriate function

* Change exception to custom package version
2018-08-14 22:06:40 +02:00
Paulus Schoutsen
486efa9aba Pin crypto (#15978)
* Pin crypto

* Fix PyJWT import once
2018-08-14 22:02:01 +02:00
Daniel Bowman
0ad9fcd8a0 Add -j$(nproc) make option to speed up build time (#15928)
Adding `-j$(nproc)` reduces build time on the external dependencies by
approximately 25%.
2018-08-14 21:28:29 +02:00
Nate Clark
c7f7912bca adds support for momentary and beep/blink switches (#15973) 2018-08-14 21:15:33 +02:00
Paulus Schoutsen
e776f88eec Use JWT for access tokens (#15972)
* Use JWT for access tokens

* Update requirements

* Improvements
2018-08-14 21:14:12 +02:00
kbickar
ee5d49a033 Added error handling for sense API timeouts (#15789)
* Added error handling for sense API timeouts

* Moved imports in function

* Moved imports to more appropriate function

* Change exception to custom package version
2018-08-14 15:50:44 +02:00
Sean Dague
051903d30c Update waterfurnace library to 0.7, add reconnect logic (#15657)
One of the features of the waterfurnace 0.7 is timingout out stuck
connections on the websocket (which tends to happen after 48 - 96
hours of operation). This requires the homeassistant component to
catch and reconnect under these circumstances. This has turned out to
be pretty robust in preventing stuck sockets over the last month.
2018-08-14 07:49:04 -04:00
Paulus Schoutsen
91e1ae035e Remove warning (#15969) 2018-08-14 12:29:55 +02:00
Fabian Affolter
10a2ecd1d6 Make setup fail if location is not available (#15967)
* Make setup fail if location is not available

* Lint
2018-08-14 12:29:31 +02:00
Khalid
800eb4d86a Fix issue when reading worxlandroid pin code (#15930)
Fixes #14050
2018-08-14 11:55:40 +02:00
Daniel Bowman
e3a2e58623 remove-phantomjs-from-docker (#15936) 2018-08-14 11:53:08 +02:00
Colin Frei
ea073b5e87 Remove unnecessary log (#15966) 2018-08-14 11:46:41 +02:00
cgtobi
619d01150f Fix google calendar documentation link. (#15968) 2018-08-14 11:44:27 +02:00
Paulus Schoutsen
6540d2e073 Switch to intermediate Mozilla cert profile (#15957)
* Allow choosing intermediate SSL profile

* Fix tests
2018-08-14 08:20:17 +02:00
Daniel Perna
69b694ff26 HomeMatic: Enable entity registry (#15950)
* Set unique ID

* Excluding setups that resolve names

* Added support for resolvenames again
2018-08-14 07:43:16 +02:00
Paulus Schoutsen
9e21765173 Bumped version to 0.76.0b1 2018-08-13 23:17:30 +02:00
Paulus Schoutsen
c0830f1c20 Deprecate remote.api (#15955) 2018-08-13 23:17:21 +02:00
Martin Hjelmare
985f96662e Upgrade pymysensors to 0.17.0 (#15942) 2018-08-13 23:17:21 +02:00
Paulus Schoutsen
e0229b799d Update frontend to 20180813.0 2018-08-13 23:11:56 +02:00
Paulus Schoutsen
3fbb56d5fb Update frontend to 20180813.0 2018-08-13 23:11:22 +02:00
Conrad Juhl Andersen
3a60c8bbed Update Xiaomi Vacuum to new StateVacuumDevice (#15643)
* Add support for states

* Woof?

* Fixed some errors

* VacuumDevice -> StateVacuumDevice

* VacuumDevice -> StateVacuumDevice

* Added split of start and pause
2018-08-13 22:50:23 +02:00
Colin Frei
f411fb89e6 Netatmo public (#15684)
* Add a sensor for netatmo public data

* A bit of cleanup before submitting pull request

* Add netatmo_public file to .coveragerc, as per pull request template instructions

* Fixes for tox complaining

* make calculations simpler, based on review feedback

* explicitly pass required_data parameter to netatmo API

* remove unnecessary spaces

* remove debug code

* code style fix
2018-08-13 22:44:20 +02:00
Paulus Schoutsen
1b5cfa7331 Deprecate remote.api (#15955) 2018-08-13 22:39:13 +02:00
Charles Garwood
39647a15ae Add monitored conditions for Unifi device_tracker (#15888)
* Add support for monitored_conditions for attributes

* Update unifi tests

* Add list of available attrs
2018-08-13 21:18:25 +02:00
Matthew Garrett
ba2e43600e Merge pull request #15959 from mjg59/eufy
Bump python-lakeside dependency
2018-08-13 10:53:44 -07:00
Matthew Garrett
c998a55fe7 Bump python-lakeside dependency
This should fix https://github.com/home-assistant/home-assistant/issues/15374
2018-08-13 10:39:48 -07:00
Jason Hu
da8f93dca2 Add trusted networks auth provider (#15812)
* Add context to login flow

* Add trusted networks auth provider

* source -> context
2018-08-13 12:40:06 +02:00
Jason Hu
50daef9a52 Add context to login flow (#15914)
* Add context to login flow

* source -> context

* Fix unit test

* Update comment
2018-08-13 11:27:18 +02:00
Jason Hu
45f12dd3c7 MQTT embedded broker has to set its own password. (#15929) 2018-08-13 11:26:06 +02:00
Hovo (Luke)
6aee535d7c Allow wait template to run the remainder of the script (#15836)
* Adding new feature to allow a wait template to run the remainer of the script on timeout

* Styling changes

* Fixing file permissions, adding test for new code

* changed variable name, refactored script to pass information into async_set_timeout

* Changing the default behaviour to continue to run the script after timeout
2018-08-13 11:23:27 +02:00
Fabian Affolter
2342709803 Upgrade beautifulsoup4 to 4.6.3 (#15946) 2018-08-13 10:52:47 +02:00
Jason Hu
272be7cdae Remove remote.API from core.Config (#15951)
* Use core.ApiConfig replace remote.API in core.Config

* Move ApiConfig to http
2018-08-13 09:26:20 +02:00
Sebastian Muszynski
31fbfed0a6 Fix magic cube support of the Aqara LAN Protocol V2 (#15940) 2018-08-13 08:17:15 +02:00
Lev Aronsky
b7486e5605 Fixed race condition in Generic Thermostat (#15784)
* Fixed race condition in Generic Thermostat

* Added a comment to clarify the meaning of the `time` argument.
2018-08-12 22:28:47 +02:00
Martin Hjelmare
e8218c4b29 Upgrade pymysensors to 0.17.0 (#15942) 2018-08-12 20:22:54 +02:00
Thom Troy
d3fed52254 Eph ember support operation modes (#15820)
* add operation mode support for climate.EphEmber

* fix linting errors from py3.5

* remove STATE_ALL_DAY and cleanup some code based on review

* use explicit None return with get

* fix none return
2018-08-12 17:48:15 +02:00
Paulus Schoutsen
1205eaaa22 Version bump to 0.77.0dev0 2018-08-11 08:59:46 +02:00
Paulus Schoutsen
69934a9598 Bumped version to 0.76.0b0 2018-08-11 08:58:52 +02:00
Paulus Schoutsen
f24773933c Update frontend to 20180811.0 2018-08-11 08:58:20 +02:00
Franck Nijhof
e17e080639 ✏️ Corrects typo in code comments (#15923)
`MomematicIP` -> `HomematicIP`
2018-08-11 08:47:41 +02:00
cgtobi
055e35b297 Add RMV public transport sensor (#15814)
* Add new public transport sensor for RMV (Rhein-Main area).

* Add required module.

* Fix naming problem.

* Add unit test.

* Update dependency version to 0.0.5.

* Add new requirements.

* Fix variable name.

* Fix issues pointed out in review.

* Remove unnecessary code.

* Fix linter error.

* Fix config value validation.

* Replace minutes as state by departure timestamp. (see ##14983)

* More work on the timestamp. (see ##14983)

* Revert timestamp work until #14983 gets merged.

* Simplify product validation.

* Remove redundant code.

* Address code change requests.

* Address more code change requests.

* Address even more code change requests.

* Simplify destination check.

* Fix linter problem.

* Bump dependency version to 0.0.7.

* Name variable more explicit.

* Only query once a minute.

* Update test case.

* Fix config validation.

* Remove unneeded import.
2018-08-10 19:35:09 +02:00
Robert Svensson
81604a9326 deCONZ - Add support for sirens (#15896)
* Add support for sirenes

* Too quick...

* Fix test

* Use siren instead of sirene
2018-08-10 19:22:12 +02:00
Paulus Schoutsen
a0e9f9f218 Merge remote-tracking branch 'origin/master' into dev 2018-08-10 18:10:56 +02:00
Paulus Schoutsen
0ab3e7a92a Add IndieAuth 4.2.2 redirect uri at client id (#15911)
* Add IndieAuth 4.2.2 redirect uri at client id

* Fix tests

* Add comment

* Limit to first 10kB of each page
2018-08-10 18:09:42 +02:00
Paulus Schoutsen
9512bb9587 Add and restore context in recorder (#15859) 2018-08-10 18:09:01 +02:00
Adam Mills
da916d7b27 Fix bug in translations upload script (#15922) 2018-08-10 11:35:01 -04:00
clayton craft
b370b6a4e4 Update radiotherm to 1.4.1 (#15910) 2018-08-10 16:10:19 +02:00
Ville Skyttä
1911168855 Misc cleanups (#15907)
* device_tracker.huawei_router: Pylint logging-not-lazy fix

* sensor.irish_rail_transport: Clean up redundant self.info test
2018-08-10 16:09:08 +02:00
Joe Lu
f98629b895 Update August component to use py-august:0.6.0 (#15916) 2018-08-10 07:27:49 +02:00
Ville Skyttä
dc01b17260 Some typing related fixes (#15899)
* Fix FlowManager.async_init handler type

It's not a Callable, but typically a key pointing to one in a dict.

* Mark pip_kwargs return type hint as Any-valued dict

install_package takes other than str args too.
2018-08-09 22:53:12 +02:00
Benoit Louy
ef61c0c3a4 Add PJLink media player platform (#15083)
* add pjlink media player component

* retrieve pjlink device name from projector if name isn't specified in configuration

* update .coveragerc

* fix style

* add missing docstrings

* address PR comments from @MartinHjelmare

* fix code style

* use snake case string for source names

* add missing period at the end of comment string

* rewrite method as function

* revert to use source name provided by projector
2018-08-09 19:58:16 +02:00
mountainsandcode
664eae72d1 Add realtime true/false switch for Waze (#15228) 2018-08-09 16:27:29 +02:00
rafale77
86658f310d Fix for multiple camera switches naming of entity (#14028)
* Fix for multiple camera switches naming of entity

appended camera name to the switch entity name.

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Add digest authentification

* Update rest_command.py

* Update config.py

* Update rest_command.py

* Update config.py
2018-08-09 15:59:23 +02:00
Mattias Welponer
a29f867908 Add HomematicIP Cloud smoke detector device (#15621)
* Add smoke detector device

* Remove not needed __init__ functions
2018-08-09 14:43:13 +02:00
Jason Hu
2233d7ca98 Fix downgrade hassio cannot get refresh_token issue (#15874)
* Fix downgrade hassio issue

* Update __init__.py
2018-08-09 13:31:48 +02:00
Jason Hu
f58425dd3c Refactor data entry flow (#15883)
* Refactoring data_entry_flow and config_entry_flow

Move SOURCE_* to config_entries
Change data_entry_flow.FlowManager.async_init() source param default
 to None
Change this first step_id as source or init if source is None
_BaseFlowManagerView pass in SOURCE_USER as default source

* First step of data entry flow decided by _async_create_flow() now

* Lint

* Change helpers.config_entry_flow.DiscoveryFlowHandler default step

* Change FlowManager.async_init source param to context dict param
2018-08-09 13:24:14 +02:00
Fabian Affolter
39d19f2183 Upgrade locationsharinglib to 2.0.11 (#15902) 2018-08-09 13:05:28 +02:00
Paulus Schoutsen
99c4c65f69 Add auth/authorize endpoint (#15887) 2018-08-09 09:27:54 +02:00
Fabian Affolter
61901496ec Upgrade pylast to 2.4.0 (#15886) 2018-08-08 22:32:21 +02:00
Steven Looman
0ab65f1ac5 Follow changes to netdisco, separating DLNA into DLNA_DMS and DLNA_DMR (#15877)
* Follow changes to netdisco, separating DLNA into DLNA_DMS and DLNA_DMR

* No uppercase for names of netdisco discoverables
2018-08-08 11:54:22 +02:00
Fabian Affolter
debdc707e9 Upgrade netdisco to 2.0.0 (#15885) 2018-08-08 11:53:43 +02:00
DubhAd
fcc918a146 Update based upon forum post (#15876)
Based upon [this post](https://community.home-assistant.io/t/device-tracker-ping-on-windows-not-working-solved/61474/3) it looks like we've found why people couldn't get the ping tracker working on Windows.
2018-08-07 18:12:36 +02:00
Fabian Affolter
b6bc0097b8 Upgrade requests_mock to 1.5.2 (#15867) 2018-08-07 16:12:16 +02:00
Fabian Affolter
d556edae31 Upgrade Sphinx to 1.7.6 (#15868) 2018-08-07 16:12:01 +02:00
Fabian Affolter
1fb2ea70c2 Upgrade asynctest to 0.12.2 (#15869) 2018-08-07 16:11:47 +02:00
Ville Skyttä
4cbcb4c3a2 Upgrade pylint to 2.1.1 (#15872) 2018-08-07 16:09:19 +02:00
Paulus Schoutsen
d071df0dec Do not make internet connection during tests (#15858)
* Do not make internet connection

* Small improvement
2018-08-07 09:27:40 +02:00
cdce8p
f09f153014 Fix HomeKit test (#15860)
* Don't raise NotImplementedError during test
2018-08-07 09:26:58 +02:00
Fabian Affolter
1d8678c431 Upgrade pysnmp to 4.4.5 (#15854) 2018-08-07 09:13:01 +02:00
Fabian Affolter
51c30980df Upgrade holidays to 0.9.6 (#15831) 2018-08-07 09:12:09 +02:00
Fabian Affolter
cb20c9b1ea Revert "Upgrade requests_mock to 1.5.2"
This reverts commit a7db2ebbe1.
2018-08-07 09:02:54 +02:00
Fabian Affolter
a7db2ebbe1 Upgrade requests_mock to 1.5.2 2018-08-07 09:01:32 +02:00
Robin
61721478f3 Add facebox auth (#15439)
* Adds auth

* Update facebox.py

* Update test_facebox.py

* Update facebox.py

* Update facebox.py

* Update facebox.py

* Update facebox.py

* Remove TIMEOUT

* Update test_facebox.py

* fix lint

* Update facebox.py

* Update test_facebox.py

* Update facebox.py

* Adds check_box_health

* Adds test auth

* Update test_facebox.py

* Update test_facebox.py

* Update test_facebox.py

* Update test_facebox.py

* Ups coverage

* Update test_facebox.py

* Update facebox.py

* Update test_facebox.py

* Update facebox.py

* Update test_facebox.py

* Update facebox.py

* Update facebox.py

* Update facebox.py
2018-08-07 07:30:36 +02:00
Paulus Schoutsen
47fa928425 Bumped version to 0.76.0.dev0 2018-08-06 13:01:32 +02:00
Paulus Schoutsen
10a7accd00 Merge branch 'master' into dev 2018-08-06 13:01:04 +02:00
psike
6031801206 Fix error when Series missing 'episodeFileCount' or 'episodeCount' (#15824)
* Fix error when Series missing 'episodeFileCount' or 'episodeCount'

* Update sonarr.py

* Update sonarr.py
2018-08-06 12:18:36 +02:00
John Arild Berentsen
9cfe0db3c8 Add different pop 012501 ID (#15838) 2018-08-06 11:10:26 +02:00
Jason Hu
8ef2cfa364 Try to fix coveralls unstable result (#15800)
* Create one tox env for code coverage report

pytest-cov generated report in project root folder, not tox env folder.

* Add cov tox env to travis

* Coveralls seems expecting all build jobs upload

* Only upload coverage after cov env success
2018-08-06 10:51:37 +02:00
Jason Hu
12e69202f8 Change to call_service async_stop non-blocking to allow service call finish (#15803)
* Call later sync_stop to allow service call finish

* Change to use non-blocking service all for restart and stop
2018-08-06 10:25:37 +02:00
ahobsonsayers
e4b2ae29bd Fix bt_home_hub_5 device tracker (#15096)
* Fix bt_home_hub_5 device tracker

Updated BT Home Hub 5 device tracker component to get it working again. The old parsing method of the DNS table has been broken for a while causing the component to fail to get connected devices. A new parsing method has been implemened and fixes all previous issues.

* Moved part of code to a published PyPi library

* Fixed Violations

* Fixed bugs in device tracker

* Moved API Specific Code to PyPi Repository

* Updated to fit requested changes, removed test as it is no longer valid and updated requirement_all.txt

* Update to fit style requirements and remove redundant code

* Removed Unnecessary Comment
2018-08-06 07:38:02 +02:00
Ryan Davies
ac4674fdb0 Add max_gps_accuracy option to Google Maps (#15833)
* Google Maps - Add max_gps_accuracy option

* Remove else statement and add continue
2018-08-06 07:17:21 +02:00
Fabian Affolter
f86702e8ab Upgrade shodan to 1.9.0 (#15839) 2018-08-05 22:48:14 +02:00
mattwing
9a84f8b763 Remove 'volume' from return dict (#15842)
https://github.com/home-assistant/home-assistant/issues/15271

intraday results do not return the volume. See https://www.alphavantage.co/documentation/#intraday
2018-08-05 22:11:51 +02:00
Dan Cinnamon
6a32b9bf87 Fix envisalink reconnect (#15832)
* Fix logic for handling connection lost/reconnect

* Fixed line length issue.
2018-08-05 18:51:23 +02:00
Steven Looman
b152becbe0 Add media_player.dlna_dmr component (#14749)
* Add media_player.dlna_dmr component

* PEP 492

* Move DIDL-template up

* Remove max_volume-override option

* Remove picky_device support

* Use DEFAULT_NAME

* Make supported_features static

* Remove unneeded argument

* Proper module-docstring

* Add http dependency

* Remove additional_configuration options, no longer used

* Change default name to 'DLNA Digital Media Renderer'

* Use python-didl-lite for DIDL-Lite-xml construction/parsing

* Handle NOT_IMPLEMENTED for UPnP state variables RelativeTimePosition and CurrentMediaDuration

* Use UPnP-UDN for unique_id

* Proper handling of upnp events

* Keeping flake8 happy

* Update requirements_all.txt

* Make UDN optional

* Ensure NotifyView is started, before using it

* Only subscribe to services we're interested in

* Don't update state_variables if value has not been changed + minor refactoring

* Improve play_media, follow flow of DLNA more closely

* Hopefully fix ClientOSError problems

* Flake8 fixes

* Keep pylint happy

* Catch errors and report gracefully

* Update async_upnp_client to 0.11.0

* Don't be so noisy

* Define/use constants for HTTP status codes

* Add discovery entry for dlna_dmr

* More robustness with regard to state variable not being set (yet)

* Keep privates hidden

* Handle NOT_IMPLEMENTED for CurrentTrackMetaData state variable

* Fixes in async_upnp_client + renew UPnP subscriptions regularly

* Not too eager

* Refactor duplicate code to _current_transport_actions and improve parsing of actions

* Support RC:1 to RC:3 and AVT:1 to AVT:3

* Moved DLNA-specifics to async_upnp_client.dlna.DmrDevice

* Use our own HTTP server to listen for events.

* More clear and explicit log message for easier troubleshooting

* Follow changes by hass, fixes traceback

* Fix not being able to do next

* Changes after review by @MartinHjelmare

* Linting

* Use homeassistant.util.get_local_ip

* Moved upnp event handling to async_upnp_client

* Keeping pylint happy

* Changes after review by @MartinHjelmare
2018-08-05 14:41:18 +02:00
Fabian Affolter
c41aa12d1d Upgrade youtube_dl to 2018.08.04 (#15837) 2018-08-05 13:29:06 +02:00
Thomas Delaet
8a81ee3b4f Velbus auto-discovery (#13742)
* remove velbus fan and light platforms

these platforms should not be there since they can be created with template components based on switch platform

* use latest version of python-velbus which supports auto-discovery of modules

* fix linting errors

* fix linting errors

* fix linting errors

* address review comments from @MartinHjelmare

* update based on automatic feedback

* fix linting errors

* update dependency

* syntax corrections

* fix lint warning

* split out common functionality in VelbusEntity
use sync methods for loading platforms
support unique_ids so that entities are registred in entity registry

* fix linting errors

* fix linting errors

* fix linting errors

* integrate review comments (common functionality in VelbusEntity class)

* rename DOMAIN import to VELBUS_DOMAIN

* revert change created by requirements script

* regen
2018-08-05 10:47:17 +02:00
fucm
5e1836f3a2 Add support for 2 Tahoma IO awning covers (#15660)
* Add Tahoma io:VerticalExteriorAwningIOComponent and io:HorizontalAwningIOComponent

* Fix position of horizontal awning cover

* Add timestamps for lock time

* Adjust open-close actions for horizontal awning cover

* Fix stop action for io:RollerShutterGenericIOComponent

* Remove redundant information

* Use get for dict lookup
2018-08-05 10:44:57 +02:00
Fabian Affolter
9ea3be4dc1 Upgrade voluptuous to 0.11.5 (#15830) 2018-08-04 17:46:14 -07:00
Martin Hjelmare
bce47eb9a4 Fix frontend requirements after bump (#15829) 2018-08-04 22:35:41 +02:00
Pascal Vizeli
018bd8544c Fix lint with wrong frontend version inside requirements_test_all 2018-08-04 22:26:13 +02:00
Pascal Vizeli
bfb9f2a00b Fix lint with wrong frontend version inside requirements_all 2018-08-04 22:24:17 +02:00
Daniel Høyer Iversen
c7a8f1143c Fix rfxtrx device id matching (#15819)
* Issue #15773

Fix PT2262 devices are incorrectly matched in rfxtrx component

* style
2018-08-04 15:23:57 +02:00
Ville Skyttä
dbe44c076e Upgrade pytest to 3.7.1 and pytest-timeout to 1.3.1 (#15809) 2018-08-04 15:22:37 +02:00
Ville Skyttä
3246b49a45 Upgrade pylint to 2.1.0 (#15811)
* Upgrade pylint to 2.1.0

* Remove no longer needed pylint disables
2018-08-04 15:22:22 +02:00
Paulus Schoutsen
0c7d46927e Bump frontend to 20180804.0 2018-08-04 15:21:11 +02:00
Jason Hu
f6935b5d27 Upgrade voluptuous-serialize to 2.0.0 (#15763)
* Upgrade voluptuous-serialize to 2.0.0

* Change to 2.0.0
2018-08-03 05:23:26 -07:00
Robert Svensson
91e8680fc5 Only report color temp when in the correct color mode (#15791) 2018-08-03 13:56:54 +02:00
Jason Hu
6f2000f5e2 Make sure use_x_forward_for and trusted_proxies must config together (#15804)
* Make sure use_x_forward_for and trusted_proxies must config together

* Fix unit test
2018-08-03 13:52:34 +02:00
Paulus Schoutsen
ee180c51cf Bump frontend to 20180803.0 2018-08-03 13:48:32 +02:00
Conrad Juhl Andersen
b63312ff2e Vacuum component: start_pause to individual start and pause commands. (#15751)
* Add start and pause to StateVacuumDevice, move start_pause to VacuumDevice

* Updated demo vacuum and tests

* Add a few more tests
2018-08-02 19:49:38 -07:00
Paulus Schoutsen
59f8a73676 Return True from Nest setup (#15797) 2018-08-02 16:36:37 -06:00
Jesse Rizzo
affd4e7df3 Add Enphase Envoy component (#15081)
* add enphase envoy component

* Add Enphase Envoy component for energy monitoring

* Fix formatting problems

* Fix formatting errors

* Fix formatting errors

* Fix formatting errors

* Change unit of measurement to W or Wh. Return sensor states as integers

* Fix formatting errors

* Fix formatting errors

* Fix formatting errors

* Move import json to update function

* Fix formatting. Add file to .coveragerc

* Add new component to requirements_all.txt

* Move API call to third party library on PyPi

* Refactor

* Run gen_requirements_all.py

* Minor refactor

* Fix indentation

* Fix indentation
2018-08-02 23:14:43 +02:00
Bryan York
38928c4c0e Fix Min/Max Kelvin color temp attribute for Google (#15697)
* Fix Min/Max Kelvin color temp attribute for Google

Max Kelvin is actually Min Mireds and vice-versa. K = 1000000 / mireds

* Update test_smart_home.py

* Update test_trait.py
2018-08-02 22:09:19 +02:00
Diogo Gomes
48af5116b3 Update pymediaroom to 0.6.4 (#15786)
* Dependency version bump

* bump version
2018-08-02 20:13:48 +02:00
Paulus Schoutsen
eb5f6efb43 Update frontend to 20180802.0 2018-08-02 14:23:40 +02:00
Paulus Schoutsen
7972d6a0c6 Update translations 2018-08-02 13:42:45 +02:00
Aaron Bach
bdea9e1333 Add support for OpenUV binary sensors and sensors (#15769)
* Initial commit

* Adjusted ownership and coverage

* Member-requested changes

* Updated Ozone to a value, not an index

* Verbiage update
2018-08-02 07:42:12 +02:00
Wim Haanstra
2f8d66ef2b RitAssist / FleetGO support (#15780)
* RitAssist / FleetGO support

* Fix lint issue
Add to .coveragerc
2018-08-02 07:01:40 +02:00
Jason Hu
589b23b7e2 Revert "Add support for STATE_AUTO of generic_thermostat (#15678)" (#15783)
This reverts commit 2e5131bb21.
2018-08-01 10:04:41 -07:00
Niklas
2e5131bb21 Add support for STATE_AUTO of generic_thermostat (#15678)
Add support for STATE_AUTO of generic_thermostat
2018-08-01 08:07:27 -07:00
Conrad Juhl Andersen
2ff5b4ce95 Add support for STATES of vacuums (#15573)
* Vacuum: Added support for STATES

* Added debug logging and corrected state order

* typo

* Fix travis error, STATE = STATE for readability

* status -> state

* Changed to Entity instead of ToogleEntity

* Updated some vacuums

* Revert changes

* Revert Changes

* added SUPPORT_STATE

* Woof?

* Implement on/off if STATE not supported

* Moved new state vaccum to Class StateVacuumDevice

* Error: I should go to bed

* Moved around methods for easier reading

* Added StateVacuumDevice demo vacuum

* Added tests for StateVacuumDevice demo vacuum

* Fix styling errors

* Refactored to BaseVaccum

* Vacuum will now go back to dock

* Class BaseVacuum is for internal use only

* return -> await

* return -> await
2018-08-01 05:51:38 -07:00
Robert Svensson
f8a478946e deCONZ - support for power plugs (#15752)
* Initial commit for deCONZ switch support

* Fix hound comment

* Fix martins comment; platforms shouldn't depend on another platform

* Fix existing tests

* New tests

* Clean up unnecessary methods

* Bump requirement to v43

* Added device state attributes to light
2018-08-01 11:03:08 +02:00
Oscar Tin Yiu Lai
623f6c841b Expose internal states and fixed on/off state of Dyson Fans (#15716)
* exposing internal state and fixed onoff state

* fixed styling

* revert file mode changes

* removed self type hints

* better unit test and changed the way to return attributes

* made wolfie happy
2018-07-31 21:38:34 -07:00
Ioan Loosley
0b6f2f5b91 Opensky altitude (#15273)
* Added Altitude to opensky

* decided to take all metadata

* Final Tidy

* More formatting

* moving CONF_ALTITUDE to platform

* Moved CONF_ALTITUDE to platform
2018-07-31 21:45:18 +02:00
Mathieu Velten
3445dc1f00 Update pynetgear to 0.4.1 (bugfixes) (#15768) 2018-07-31 21:40:13 +02:00
Fabian Affolter
a11c2a0bd8 Fix docstrings (#15770) 2018-07-31 21:39:37 +02:00
Diogo Gomes
95da41aa15 This component API has been decomissioned on the 31st of May 2018 by Telstra (#15757)
See #15668
2018-07-31 21:27:43 +02:00
Fabian Affolter
27401f4975 Upgrade Mastodon.py to 1.3.1 (#15766) 2018-07-31 21:17:55 +02:00
Scott Albertson
d902a9f279 Add a "Reviewed by Hound" badge (#15767) 2018-07-31 21:17:33 +02:00
priiduonu
03847e6c41 Round precipitation forecast to 1 decimal place (#15759)
The OWM returns precipitation forecast values as they are submitted to their network. It can lead to values like `0.0025000000000004 mm` which does not make sense and messes up the display card. This PR rounds the value to 1 decimal place.
2018-07-31 19:18:11 +02:00
Fabian Affolter
a4f9602405 Convert wind speed to km/h (fixes #15710) (#15740)
* Convert wind speed to km/h (fixes #15710)

* Round speed
2018-07-31 19:11:29 +02:00
John Arild Berentsen
5f214ffa98 Update pyozw to 0.4.9 (#15758)
* update pyozw to 0.4.8

* add requirements_all.txt

* use 0.4.9
2018-07-31 15:14:14 +01:00
Andrey
8ee3b535ef Add disallow_untyped_calls to mypy check. (#15661)
* Add disallow_untyped_calls to mypy check.

* Fix generator
2018-07-31 15:00:17 +01:00
Andrey Kupreychik
951372491c Fixed NDMS for latest firmware (#15511)
* Fixed NDMS for latest firmware.
Now using telnet instead of Web Interface

* Using external library for NDMS interactions

* updated requirements_all

* renamed `mac` to `device` back

* Using generators for name and attributes fetching
2018-07-31 11:14:49 +02:00
Jason Hu
eeb79476de Decouple login flow view and data entry flow view (#15715) 2018-07-30 21:59:18 -07:00
Aaron Bach
1b2d0e7a6f Better handling of Yi camera being disconnected (#15754)
* Better handling of Yi camera being disconnected

* Handling video processing as well

* Cleanup

* Member-requested changes

* Member-requested changes
2018-07-30 21:56:52 -07:00
Teemu R
3208ad27ac Add kodi unique id based on discovery (#15093)
* kodi add unique id based on discovery

* initialize unique_id to None

* use netdisco-extracted mac_address

* use an uuid instead of mac for real uniqueness

* add missing docstring

* verify that there is no entity already for the given unique id

* whitespace fix
2018-07-30 17:09:38 +02:00
superpuffin
cf87b76b0c Upgrade Adafruit-DHT to 1.3.3 (#15706)
* Change to newer pip package

The package Adafruit_Python_DHT==1.3.2 was broken and would not install, breaking DHT sensor support in Home assistant. It has since been fixed in Adafruit-DHT==1.3.3.

See: https://github.com/adafruit/Adafruit_Python_DHT/issues/99

* Update requirements_all.txt

New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.

* Comment out Adafruit-DHT

Adafruit_Python_DHT changed name to Adafruit-DHT, which still need pyx support breaking our CI, need to be comment out.

* Update requirements_all.txt
2018-07-30 15:15:13 +01:00
1394 changed files with 16035 additions and 7995 deletions

View File

@@ -104,6 +104,9 @@ omit =
homeassistant/components/fritzbox.py
homeassistant/components/switch/fritzbox.py
homeassistant/components/ecovacs.py
homeassistant/components/*/ecovacs.py
homeassistant/components/eufy.py
homeassistant/components/*/eufy.py
@@ -113,6 +116,12 @@ omit =
homeassistant/components/google.py
homeassistant/components/*/google.py
homeassistant/components/hangouts/__init__.py
homeassistant/components/hangouts/const.py
homeassistant/components/hangouts/hangouts_bot.py
homeassistant/components/hangouts/hangups_utils.py
homeassistant/components/*/hangouts.py
homeassistant/components/hdmi_cec.py
homeassistant/components/*/hdmi_cec.py
@@ -133,12 +142,13 @@ omit =
homeassistant/components/ihc/*
homeassistant/components/*/ihc.py
homeassistant/components/insteon/*
homeassistant/components/*/insteon.py
homeassistant/components/insteon_local.py
homeassistant/components/*/insteon_local.py
homeassistant/components/insteon_plm/*
homeassistant/components/*/insteon_plm.py
homeassistant/components/insteon_plm.py
homeassistant/components/ios.py
homeassistant/components/*/ios.py
@@ -215,6 +225,9 @@ omit =
homeassistant/components/opencv.py
homeassistant/components/*/opencv.py
homeassistant/components/openuv.py
homeassistant/components/*/openuv.py
homeassistant/components/pilight.py
homeassistant/components/*/pilight.py
@@ -440,6 +453,7 @@ omit =
homeassistant/components/device_tracker/netgear.py
homeassistant/components/device_tracker/nmap_tracker.py
homeassistant/components/device_tracker/ping.py
homeassistant/components/device_tracker/ritassist.py
homeassistant/components/device_tracker/sky_hub.py
homeassistant/components/device_tracker/snmp.py
homeassistant/components/device_tracker/swisscom.py
@@ -509,6 +523,7 @@ omit =
homeassistant/components/media_player/denon.py
homeassistant/components/media_player/denonavr.py
homeassistant/components/media_player/directv.py
homeassistant/components/media_player/dlna_dmr.py
homeassistant/components/media_player/dunehd.py
homeassistant/components/media_player/emby.py
homeassistant/components/media_player/epson.py
@@ -532,6 +547,7 @@ omit =
homeassistant/components/media_player/pandora.py
homeassistant/components/media_player/philips_js.py
homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/pjlink.py
homeassistant/components/media_player/plex.py
homeassistant/components/media_player/roku.py
homeassistant/components/media_player/russound_rio.py
@@ -632,6 +648,7 @@ omit =
homeassistant/components/sensor/eddystone_temperature.py
homeassistant/components/sensor/eliqonline.py
homeassistant/components/sensor/emoncms.py
homeassistant/components/sensor/enphase_envoy.py
homeassistant/components/sensor/envirophat.py
homeassistant/components/sensor/etherscan.py
homeassistant/components/sensor/fastdotcom.py
@@ -676,7 +693,9 @@ omit =
homeassistant/components/sensor/mvglive.py
homeassistant/components/sensor/nederlandse_spoorwegen.py
homeassistant/components/sensor/netdata.py
homeassistant/components/sensor/netdata_public.py
homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/noaa_tides.py
homeassistant/components/sensor/nsw_fuel_station.py
homeassistant/components/sensor/nut.py
homeassistant/components/sensor/nzbget.py

View File

@@ -13,7 +13,8 @@ matrix:
- python: "3.5.3"
env: TOXENV=typing
- python: "3.5.3"
env: TOXENV=py35
env: TOXENV=cov
after_success: coveralls
- python: "3.6"
env: TOXENV=py36
- python: "3.7"
@@ -45,4 +46,3 @@ deploy:
on:
branch: dev
condition: $TOXENV = lint
after_success: coveralls

View File

@@ -87,6 +87,8 @@ homeassistant/components/*/axis.py @kane610
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/*/deconz.py @kane610
homeassistant/components/ecovacs.py @OverloadUT
homeassistant/components/*/ecovacs.py @OverloadUT
homeassistant/components/eight_sleep.py @mezz64
homeassistant/components/*/eight_sleep.py @mezz64
homeassistant/components/hive.py @Rendili @KJonline
@@ -98,6 +100,8 @@ homeassistant/components/konnected.py @heythisisnate
homeassistant/components/*/konnected.py @heythisisnate
homeassistant/components/matrix.py @tinloaf
homeassistant/components/*/matrix.py @tinloaf
homeassistant/components/openuv.py @bachya
homeassistant/components/*/openuv.py @bachya
homeassistant/components/qwikswitch.py @kellerza
homeassistant/components/*/qwikswitch.py @kellerza
homeassistant/components/rainmachine/* @bachya

View File

@@ -10,7 +10,6 @@ LABEL maintainer="Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
#ENV INSTALL_OPENALPR no
#ENV INSTALL_FFMPEG no
#ENV INSTALL_LIBCEC no
#ENV INSTALL_PHANTOMJS no
#ENV INSTALL_SSOCR no
#ENV INSTALL_IPERF3 no

View File

@@ -1,5 +1,5 @@
Home Assistant |Build Status| |Coverage Status| |Chat Status|
=============================================================
Home Assistant |Build Status| |Coverage Status| |Chat Status| |Reviewed by Hound|
=================================================================================
Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control.
@@ -33,6 +33,8 @@ of a component, check the `Home Assistant help section <https://home-assistant.i
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
:target: https://discord.gg/c5DvZ4e
.. |Reviewed by Hound| image:: https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg
:target: https://houndci.com
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
:target: https://home-assistant.io/demo/
.. |screenshot-components| image:: https://raw.github.com/home-assistant/home-assistant/dev/docs/screenshot-components.png

View File

@@ -60,14 +60,6 @@ loader module
:undoc-members:
:show-inheritance:
remote module
---------------------------
.. automodule:: homeassistant.remote
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------

View File

@@ -2,64 +2,83 @@
import asyncio
import logging
from collections import OrderedDict
from typing import Any, Dict, List, Optional, Tuple, cast
import jwt
from homeassistant import data_entry_flow
from homeassistant.core import callback
from homeassistant.core import callback, HomeAssistant
from homeassistant.util import dt as dt_util
from . import models
from . import auth_store
from .providers import auth_provider_from_config
from . import auth_store, models
from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule
from .providers import auth_provider_from_config, AuthProvider, LoginFlow
_LOGGER = logging.getLogger(__name__)
_MfaModuleDict = Dict[str, MultiFactorAuthModule]
_ProviderKey = Tuple[str, Optional[str]]
_ProviderDict = Dict[_ProviderKey, AuthProvider]
async def auth_manager_from_config(hass, provider_configs):
"""Initialize an auth manager from config."""
async def auth_manager_from_config(
hass: HomeAssistant,
provider_configs: List[Dict[str, Any]],
module_configs: List[Dict[str, Any]]) -> 'AuthManager':
"""Initialize an auth manager from config.
CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or
mfa modules exist in configs.
"""
store = auth_store.AuthStore(hass)
if provider_configs:
providers = await asyncio.gather(
*[auth_provider_from_config(hass, store, config)
for config in provider_configs])
else:
providers = []
providers = ()
# So returned auth providers are in same order as config
provider_hash = OrderedDict()
provider_hash = OrderedDict() # type: _ProviderDict
for provider in providers:
if provider is None:
continue
key = (provider.type, provider.id)
if key in provider_hash:
_LOGGER.error(
'Found duplicate provider: %s. Please add unique IDs if you '
'want to have the same provider twice.', key)
continue
provider_hash[key] = provider
manager = AuthManager(hass, store, provider_hash)
if module_configs:
modules = await asyncio.gather(
*[auth_mfa_module_from_config(hass, config)
for config in module_configs])
else:
modules = ()
# So returned auth modules are in same order as config
module_hash = OrderedDict() # type: _MfaModuleDict
for module in modules:
module_hash[module.id] = module
manager = AuthManager(hass, store, provider_hash, module_hash)
return manager
class AuthManager:
"""Manage the authentication for Home Assistant."""
def __init__(self, hass, store, providers):
def __init__(self, hass: HomeAssistant, store: auth_store.AuthStore,
providers: _ProviderDict, mfa_modules: _MfaModuleDict) \
-> None:
"""Initialize the auth manager."""
self.hass = hass
self._store = store
self._providers = providers
self._mfa_modules = mfa_modules
self.login_flow = data_entry_flow.FlowManager(
hass, self._async_create_login_flow,
self._async_finish_login_flow)
self._access_tokens = OrderedDict()
@property
def active(self):
def active(self) -> bool:
"""Return if any auth providers are registered."""
return bool(self._providers)
@property
def support_legacy(self):
def support_legacy(self) -> bool:
"""
Return if legacy_api_password auth providers are registered.
@@ -71,19 +90,39 @@ class AuthManager:
return False
@property
def auth_providers(self):
def auth_providers(self) -> List[AuthProvider]:
"""Return a list of available auth providers."""
return list(self._providers.values())
async def async_get_users(self):
@property
def auth_mfa_modules(self) -> List[MultiFactorAuthModule]:
"""Return a list of available auth modules."""
return list(self._mfa_modules.values())
def get_auth_mfa_module(self, module_id: str) \
-> Optional[MultiFactorAuthModule]:
"""Return an multi-factor auth module, None if not found."""
return self._mfa_modules.get(module_id)
async def async_get_users(self) -> List[models.User]:
"""Retrieve all users."""
return await self._store.async_get_users()
async def async_get_user(self, user_id):
async def async_get_user(self, user_id: str) -> Optional[models.User]:
"""Retrieve a user."""
return await self._store.async_get_user(user_id)
async def async_create_system_user(self, name):
async def async_get_user_by_credentials(
self, credentials: models.Credentials) -> Optional[models.User]:
"""Get a user by credential, return None if not found."""
for user in await self.async_get_users():
for creds in user.credentials:
if creds.id == credentials.id:
return user
return None
async def async_create_system_user(self, name: str) -> models.User:
"""Create a system user."""
return await self._store.async_create_user(
name=name,
@@ -91,27 +130,27 @@ class AuthManager:
is_active=True,
)
async def async_create_user(self, name):
async def async_create_user(self, name: str) -> models.User:
"""Create a user."""
kwargs = {
'name': name,
'is_active': True,
}
} # type: Dict[str, Any]
if await self._user_should_be_owner():
kwargs['is_owner'] = True
return await self._store.async_create_user(**kwargs)
async def async_get_or_create_user(self, credentials):
async def async_get_or_create_user(self, credentials: models.Credentials) \
-> models.User:
"""Get or create a user."""
if not credentials.is_new:
for user in await self._store.async_get_users():
for creds in user.credentials:
if creds.id == credentials.id:
return user
raise ValueError('Unable to find the user.')
user = await self.async_get_user_by_credentials(credentials)
if user is None:
raise ValueError('Unable to find the user.')
else:
return user
auth_provider = self._async_get_auth_provider(credentials)
@@ -123,15 +162,16 @@ class AuthManager:
return await self._store.async_create_user(
credentials=credentials,
name=info.get('name'),
is_active=info.get('is_active', False)
name=info.name,
is_active=info.is_active,
)
async def async_link_user(self, user, credentials):
async def async_link_user(self, user: models.User,
credentials: models.Credentials) -> None:
"""Link credentials to an existing user."""
await self._store.async_link_user(user, credentials)
async def async_remove_user(self, user):
async def async_remove_user(self, user: models.User) -> None:
"""Remove a user."""
tasks = [
self.async_remove_credentials(credentials)
@@ -143,27 +183,68 @@ class AuthManager:
await self._store.async_remove_user(user)
async def async_activate_user(self, user):
async def async_activate_user(self, user: models.User) -> None:
"""Activate a user."""
await self._store.async_activate_user(user)
async def async_deactivate_user(self, user):
async def async_deactivate_user(self, user: models.User) -> None:
"""Deactivate a user."""
if user.is_owner:
raise ValueError('Unable to deactive the owner')
await self._store.async_deactivate_user(user)
async def async_remove_credentials(self, credentials):
async def async_remove_credentials(
self, credentials: models.Credentials) -> None:
"""Remove credentials."""
provider = self._async_get_auth_provider(credentials)
if (provider is not None and
hasattr(provider, 'async_will_remove_credentials')):
await provider.async_will_remove_credentials(credentials)
# https://github.com/python/mypy/issues/1424
await provider.async_will_remove_credentials( # type: ignore
credentials)
await self._store.async_remove_credentials(credentials)
async def async_create_refresh_token(self, user, client_id=None):
async def async_enable_user_mfa(self, user: models.User,
mfa_module_id: str, data: Any) -> None:
"""Enable a multi-factor auth module for user."""
if user.system_generated:
raise ValueError('System generated users cannot enable '
'multi-factor auth module.')
module = self.get_auth_mfa_module(mfa_module_id)
if module is None:
raise ValueError('Unable find multi-factor auth module: {}'
.format(mfa_module_id))
await module.async_setup_user(user.id, data)
async def async_disable_user_mfa(self, user: models.User,
mfa_module_id: str) -> None:
"""Disable a multi-factor auth module for user."""
if user.system_generated:
raise ValueError('System generated users cannot disable '
'multi-factor auth module.')
module = self.get_auth_mfa_module(mfa_module_id)
if module is None:
raise ValueError('Unable find multi-factor auth module: {}'
.format(mfa_module_id))
await module.async_depose_user(user.id)
async def async_get_enabled_mfa(self, user: models.User) -> Dict[str, str]:
"""List enabled mfa modules for user."""
modules = OrderedDict() # type: Dict[str, str]
for module_id, module in self._mfa_modules.items():
if await module.async_is_user_setup(user.id):
modules[module_id] = module.name
return modules
async def async_create_refresh_token(self, user: models.User,
client_id: Optional[str] = None) \
-> models.RefreshToken:
"""Create a new refresh token for a user."""
if not user.is_active:
raise ValueError('User is not active')
@@ -178,59 +259,119 @@ class AuthManager:
return await self._store.async_create_refresh_token(user, client_id)
async def async_get_refresh_token(self, token):
async def async_get_refresh_token(
self, token_id: str) -> Optional[models.RefreshToken]:
"""Get refresh token by id."""
return await self._store.async_get_refresh_token(token_id)
async def async_get_refresh_token_by_token(
self, token: str) -> Optional[models.RefreshToken]:
"""Get refresh token by token."""
return await self._store.async_get_refresh_token(token)
return await self._store.async_get_refresh_token_by_token(token)
async def async_remove_refresh_token(self,
refresh_token: models.RefreshToken) \
-> None:
"""Delete a refresh token."""
await self._store.async_remove_refresh_token(refresh_token)
@callback
def async_create_access_token(self, refresh_token):
def async_create_access_token(self,
refresh_token: models.RefreshToken) -> str:
"""Create a new access token."""
access_token = models.AccessToken(refresh_token=refresh_token)
self._access_tokens[access_token.token] = access_token
return access_token
# pylint: disable=no-self-use
return jwt.encode({
'iss': refresh_token.id,
'iat': dt_util.utcnow(),
'exp': dt_util.utcnow() + refresh_token.access_token_expiration,
}, refresh_token.jwt_key, algorithm='HS256').decode()
@callback
def async_get_access_token(self, token):
"""Get an access token."""
tkn = self._access_tokens.get(token)
if tkn is None:
_LOGGER.debug('Attempt to get non-existing access token')
async def async_validate_access_token(
self, token: str) -> Optional[models.RefreshToken]:
"""Return refresh token if an access token is valid."""
try:
unverif_claims = jwt.decode(token, verify=False)
except jwt.InvalidTokenError:
return None
if tkn.expired or not tkn.refresh_token.user.is_active:
if tkn.expired:
_LOGGER.debug('Attempt to get expired access token')
else:
_LOGGER.debug('Attempt to get access token for inactive user')
self._access_tokens.pop(token)
refresh_token = await self.async_get_refresh_token(
cast(str, unverif_claims.get('iss')))
if refresh_token is None:
jwt_key = ''
issuer = ''
else:
jwt_key = refresh_token.jwt_key
issuer = refresh_token.id
try:
jwt.decode(
token,
jwt_key,
leeway=10,
issuer=issuer,
algorithms=['HS256']
)
except jwt.InvalidTokenError:
return None
return tkn
if refresh_token is None or not refresh_token.user.is_active:
return None
async def _async_create_login_flow(self, handler, *, source, data):
return refresh_token
async def _async_create_login_flow(
self, handler: _ProviderKey, *, context: Optional[Dict],
data: Optional[Any]) -> data_entry_flow.FlowHandler:
"""Create a login flow."""
auth_provider = self._providers[handler]
return await auth_provider.async_credential_flow()
return await auth_provider.async_login_flow(context)
async def _async_finish_login_flow(self, result):
"""Result of a credential login flow."""
async def _async_finish_login_flow(
self, flow: LoginFlow, result: Dict[str, Any]) \
-> Dict[str, Any]:
"""Return a user as result of login flow."""
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return None
return result
# we got final result
if isinstance(result['data'], models.User):
result['result'] = result['data']
return result
auth_provider = self._providers[result['handler']]
return await auth_provider.async_get_or_create_credentials(
credentials = await auth_provider.async_get_or_create_credentials(
result['data'])
if flow.context is not None and flow.context.get('credential_only'):
result['result'] = credentials
return result
# multi-factor module cannot enabled for new credential
# which has not linked to a user yet
if auth_provider.support_mfa and not credentials.is_new:
user = await self.async_get_user_by_credentials(credentials)
if user is not None:
modules = await self.async_get_enabled_mfa(user)
if modules:
flow.user = user
flow.available_mfa_modules = modules
return await flow.async_step_select_mfa_module()
result['result'] = await self.async_get_or_create_user(credentials)
return result
@callback
def _async_get_auth_provider(self, credentials):
"""Helper to get auth provider from a set of credentials."""
def _async_get_auth_provider(
self, credentials: models.Credentials) -> Optional[AuthProvider]:
"""Get auth provider from a set of credentials."""
auth_provider_key = (credentials.auth_provider_type,
credentials.auth_provider_id)
return self._providers.get(auth_provider_key)
async def _user_should_be_owner(self):
async def _user_should_be_owner(self) -> bool:
"""Determine if user should be owner.
A user should be an owner if it is the first non-system user that is

View File

@@ -1,7 +1,11 @@
"""Storage for auth models."""
from collections import OrderedDict
from datetime import timedelta
from logging import getLogger
from typing import Any, Dict, List, Optional # noqa: F401
import hmac
from homeassistant.core import HomeAssistant, callback
from homeassistant.util import dt as dt_util
from . import models
@@ -19,35 +23,41 @@ class AuthStore:
called that needs it.
"""
def __init__(self, hass):
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the auth store."""
self.hass = hass
self._users = None
self._users = None # type: Optional[Dict[str, models.User]]
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
async def async_get_users(self):
async def async_get_users(self) -> List[models.User]:
"""Retrieve all users."""
if self._users is None:
await self.async_load()
await self._async_load()
assert self._users is not None
return list(self._users.values())
async def async_get_user(self, user_id):
async def async_get_user(self, user_id: str) -> Optional[models.User]:
"""Retrieve a user by id."""
if self._users is None:
await self.async_load()
await self._async_load()
assert self._users is not None
return self._users.get(user_id)
async def async_create_user(self, name, is_owner=None, is_active=None,
system_generated=None, credentials=None):
async def async_create_user(
self, name: Optional[str], is_owner: Optional[bool] = None,
is_active: Optional[bool] = None,
system_generated: Optional[bool] = None,
credentials: Optional[models.Credentials] = None) -> models.User:
"""Create a new user."""
if self._users is None:
await self.async_load()
await self._async_load()
assert self._users is not None
kwargs = {
'name': name
}
} # type: Dict[str, Any]
if is_owner is not None:
kwargs['is_owner'] = is_owner
@@ -63,36 +73,46 @@ class AuthStore:
self._users[new_user.id] = new_user
if credentials is None:
await self.async_save()
self._async_schedule_save()
return new_user
# Saving is done inside the link.
await self.async_link_user(new_user, credentials)
return new_user
async def async_link_user(self, user, credentials):
async def async_link_user(self, user: models.User,
credentials: models.Credentials) -> None:
"""Add credentials to an existing user."""
user.credentials.append(credentials)
await self.async_save()
self._async_schedule_save()
credentials.is_new = False
async def async_remove_user(self, user):
async def async_remove_user(self, user: models.User) -> None:
"""Remove a user."""
self._users.pop(user.id)
await self.async_save()
if self._users is None:
await self._async_load()
assert self._users is not None
async def async_activate_user(self, user):
self._users.pop(user.id)
self._async_schedule_save()
async def async_activate_user(self, user: models.User) -> None:
"""Activate a user."""
user.is_active = True
await self.async_save()
self._async_schedule_save()
async def async_deactivate_user(self, user):
async def async_deactivate_user(self, user: models.User) -> None:
"""Activate a user."""
user.is_active = False
await self.async_save()
self._async_schedule_save()
async def async_remove_credentials(self, credentials):
async def async_remove_credentials(
self, credentials: models.Credentials) -> None:
"""Remove credentials."""
if self._users is None:
await self._async_load()
assert self._users is not None
for user in self._users.values():
found = None
@@ -105,28 +125,60 @@ class AuthStore:
user.credentials.pop(found)
break
await self.async_save()
self._async_schedule_save()
async def async_create_refresh_token(self, user, client_id=None):
async def async_create_refresh_token(
self, user: models.User, client_id: Optional[str] = None) \
-> models.RefreshToken:
"""Create a new token for a user."""
refresh_token = models.RefreshToken(user=user, client_id=client_id)
user.refresh_tokens[refresh_token.token] = refresh_token
await self.async_save()
user.refresh_tokens[refresh_token.id] = refresh_token
self._async_schedule_save()
return refresh_token
async def async_get_refresh_token(self, token):
"""Get refresh token by token."""
async def async_remove_refresh_token(
self, refresh_token: models.RefreshToken) -> None:
"""Remove a refresh token."""
if self._users is None:
await self.async_load()
await self._async_load()
assert self._users is not None
for user in self._users.values():
refresh_token = user.refresh_tokens.get(token)
if user.refresh_tokens.pop(refresh_token.id, None):
self._async_schedule_save()
break
async def async_get_refresh_token(
self, token_id: str) -> Optional[models.RefreshToken]:
"""Get refresh token by id."""
if self._users is None:
await self._async_load()
assert self._users is not None
for user in self._users.values():
refresh_token = user.refresh_tokens.get(token_id)
if refresh_token is not None:
return refresh_token
return None
async def async_load(self):
async def async_get_refresh_token_by_token(
self, token: str) -> Optional[models.RefreshToken]:
"""Get refresh token by token."""
if self._users is None:
await self._async_load()
assert self._users is not None
found = None
for user in self._users.values():
for refresh_token in user.refresh_tokens.values():
if hmac.compare_digest(refresh_token.token, token):
found = refresh_token
return found
async def _async_load(self) -> None:
"""Load the users."""
data = await self._store.async_load()
@@ -135,7 +187,7 @@ class AuthStore:
if self._users is not None:
return
users = OrderedDict()
users = OrderedDict() # type: Dict[str, models.User]
if data is None:
self._users = users
@@ -153,34 +205,44 @@ class AuthStore:
data=cred_dict['data'],
))
refresh_tokens = OrderedDict()
for rt_dict in data['refresh_tokens']:
# Filter out the old keys that don't have jwt_key (pre-0.76)
if 'jwt_key' not in rt_dict:
continue
created_at = dt_util.parse_datetime(rt_dict['created_at'])
if created_at is None:
getLogger(__name__).error(
'Ignoring refresh token %(id)s with invalid created_at '
'%(created_at)s for user_id %(user_id)s', rt_dict)
continue
token = models.RefreshToken(
id=rt_dict['id'],
user=users[rt_dict['user_id']],
client_id=rt_dict['client_id'],
created_at=dt_util.parse_datetime(rt_dict['created_at']),
created_at=created_at,
access_token_expiration=timedelta(
seconds=rt_dict['access_token_expiration']),
token=rt_dict['token'],
jwt_key=rt_dict['jwt_key']
)
refresh_tokens[token.id] = token
users[rt_dict['user_id']].refresh_tokens[token.token] = token
for ac_dict in data['access_tokens']:
refresh_token = refresh_tokens[ac_dict['refresh_token_id']]
token = models.AccessToken(
refresh_token=refresh_token,
created_at=dt_util.parse_datetime(ac_dict['created_at']),
token=ac_dict['token'],
)
refresh_token.access_tokens.append(token)
users[rt_dict['user_id']].refresh_tokens[token.id] = token
self._users = users
async def async_save(self):
@callback
def _async_schedule_save(self) -> None:
"""Save users."""
if self._users is None:
return
self._store.async_delay_save(self._data_to_save, 1)
@callback
def _data_to_save(self) -> Dict:
"""Return the data to store."""
assert self._users is not None
users = [
{
'id': user.id,
@@ -213,28 +275,14 @@ class AuthStore:
'access_token_expiration':
refresh_token.access_token_expiration.total_seconds(),
'token': refresh_token.token,
'jwt_key': refresh_token.jwt_key,
}
for user in self._users.values()
for refresh_token in user.refresh_tokens.values()
]
access_tokens = [
{
'id': user.id,
'refresh_token_id': refresh_token.id,
'created_at': access_token.created_at.isoformat(),
'token': access_token.token,
}
for user in self._users.values()
for refresh_token in user.refresh_tokens.values()
for access_token in refresh_token.access_tokens
]
data = {
return {
'users': users,
'credentials': credentials,
'access_tokens': access_tokens,
'refresh_tokens': refresh_tokens,
}
await self._store.async_save(data, delay=1)

View File

@@ -0,0 +1,177 @@
"""Plugable auth modules for Home Assistant."""
from datetime import timedelta
import importlib
import logging
import types
from typing import Any, Dict, Optional
import voluptuous as vol
from voluptuous.humanize import humanize_error
from homeassistant import requirements, data_entry_flow
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.decorator import Registry
MULTI_FACTOR_AUTH_MODULES = Registry()
MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema({
vol.Required(CONF_TYPE): str,
vol.Optional(CONF_NAME): str,
# Specify ID if you have two mfa auth module for same type.
vol.Optional(CONF_ID): str,
}, extra=vol.ALLOW_EXTRA)
SESSION_EXPIRATION = timedelta(minutes=5)
DATA_REQS = 'mfa_auth_module_reqs_processed'
_LOGGER = logging.getLogger(__name__)
class MultiFactorAuthModule:
"""Multi-factor Auth Module of validation function."""
DEFAULT_TITLE = 'Unnamed auth module'
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
"""Initialize an auth module."""
self.hass = hass
self.config = config
@property
def id(self) -> str: # pylint: disable=invalid-name
"""Return id of the auth module.
Default is same as type
"""
return self.config.get(CONF_ID, self.type)
@property
def type(self) -> str:
"""Return type of the module."""
return self.config[CONF_TYPE] # type: ignore
@property
def name(self) -> str:
"""Return the name of the auth module."""
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
# Implement by extending class
@property
def input_schema(self) -> vol.Schema:
"""Return a voluptuous schema to define mfa auth module's input."""
raise NotImplementedError
async def async_setup_flow(self, user_id: str) -> 'SetupFlow':
"""Return a data entry flow handler for setup module.
Mfa module should extend SetupFlow
"""
raise NotImplementedError
async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
"""Set up user for mfa auth module."""
raise NotImplementedError
async def async_depose_user(self, user_id: str) -> None:
"""Remove user from mfa module."""
raise NotImplementedError
async def async_is_user_setup(self, user_id: str) -> bool:
"""Return whether user is setup."""
raise NotImplementedError
async def async_validation(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
raise NotImplementedError
class SetupFlow(data_entry_flow.FlowHandler):
"""Handler for the setup flow."""
def __init__(self, auth_module: MultiFactorAuthModule,
setup_schema: vol.Schema,
user_id: str) -> None:
"""Initialize the setup flow."""
self._auth_module = auth_module
self._setup_schema = setup_schema
self._user_id = user_id
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the first step of setup flow.
Return self.async_show_form(step_id='init') if user_input == None.
Return self.async_create_entry(data={'result': result}) if finish.
"""
errors = {} # type: Dict[str, str]
if user_input:
result = await self._auth_module.async_setup_user(
self._user_id, user_input)
return self.async_create_entry(
title=self._auth_module.name,
data={'result': result}
)
return self.async_show_form(
step_id='init',
data_schema=self._setup_schema,
errors=errors
)
async def auth_mfa_module_from_config(
hass: HomeAssistant, config: Dict[str, Any]) \
-> MultiFactorAuthModule:
"""Initialize an auth module from a config."""
module_name = config[CONF_TYPE]
module = await _load_mfa_module(hass, module_name)
try:
config = module.CONFIG_SCHEMA(config) # type: ignore
except vol.Invalid as err:
_LOGGER.error('Invalid configuration for multi-factor module %s: %s',
module_name, humanize_error(config, err))
raise
return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) # type: ignore
async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
-> types.ModuleType:
"""Load an mfa auth module."""
module_path = 'homeassistant.auth.mfa_modules.{}'.format(module_name)
try:
module = importlib.import_module(module_path)
except ImportError as err:
_LOGGER.error('Unable to load mfa module %s: %s', module_name, err)
raise HomeAssistantError('Unable to load mfa module {}: {}'.format(
module_name, err))
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
return module
processed = hass.data.get(DATA_REQS)
if processed and module_name in processed:
return module
processed = hass.data[DATA_REQS] = set()
# https://github.com/python/mypy/issues/1424
req_success = await requirements.async_process_requirements(
hass, module_path, module.REQUIREMENTS) # type: ignore
if not req_success:
raise HomeAssistantError(
'Unable to process requirements of mfa module {}'.format(
module_name))
processed.add(module_name)
return module

View File

@@ -0,0 +1,89 @@
"""Example auth module."""
import logging
from typing import Any, Dict
import voluptuous as vol
from homeassistant.core import HomeAssistant
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
vol.Required('data'): [vol.Schema({
vol.Required('user_id'): str,
vol.Required('pin'): str,
})]
}, extra=vol.PREVENT_EXTRA)
_LOGGER = logging.getLogger(__name__)
@MULTI_FACTOR_AUTH_MODULES.register('insecure_example')
class InsecureExampleModule(MultiFactorAuthModule):
"""Example auth module validate pin."""
DEFAULT_TITLE = 'Insecure Personal Identify Number'
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
"""Initialize the user data store."""
super().__init__(hass, config)
self._data = config['data']
@property
def input_schema(self) -> vol.Schema:
"""Validate login flow input data."""
return vol.Schema({'pin': str})
@property
def setup_schema(self) -> vol.Schema:
"""Validate async_setup_user input data."""
return vol.Schema({'pin': str})
async def async_setup_flow(self, user_id: str) -> SetupFlow:
"""Return a data entry flow handler for setup module.
Mfa module should extend SetupFlow
"""
return SetupFlow(self, self.setup_schema, user_id)
async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
"""Set up user to use mfa module."""
# data shall has been validate in caller
pin = setup_data['pin']
for data in self._data:
if data['user_id'] == user_id:
# already setup, override
data['pin'] = pin
return
self._data.append({'user_id': user_id, 'pin': pin})
async def async_depose_user(self, user_id: str) -> None:
"""Remove user from mfa module."""
found = None
for data in self._data:
if data['user_id'] == user_id:
found = data
break
if found:
self._data.remove(found)
async def async_is_user_setup(self, user_id: str) -> bool:
"""Return whether user is setup."""
for data in self._data:
if data['user_id'] == user_id:
return True
return False
async def async_validation(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
for data in self._data:
if data['user_id'] == user_id:
# user_input has been validate in caller
if data['pin'] == user_input['pin']:
return True
return False

View File

@@ -0,0 +1,213 @@
"""Time-based One Time Password auth module."""
import logging
from io import BytesIO
from typing import Any, Dict, Optional, Tuple # noqa: F401
import voluptuous as vol
from homeassistant.auth.models import User
from homeassistant.core import HomeAssistant
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
REQUIREMENTS = ['pyotp==2.2.6', 'PyQRCode==1.2.1']
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA)
STORAGE_VERSION = 1
STORAGE_KEY = 'auth_module.totp'
STORAGE_USERS = 'users'
STORAGE_USER_ID = 'user_id'
STORAGE_OTA_SECRET = 'ota_secret'
INPUT_FIELD_CODE = 'code'
DUMMY_SECRET = 'FPPTH34D4E3MI2HG'
_LOGGER = logging.getLogger(__name__)
def _generate_qr_code(data: str) -> str:
"""Generate a base64 PNG string represent QR Code image of data."""
import pyqrcode
qr_code = pyqrcode.create(data)
with BytesIO() as buffer:
qr_code.svg(file=buffer, scale=4)
return '{}'.format(
buffer.getvalue().decode("ascii").replace('\n', '')
.replace('<?xml version="1.0" encoding="UTF-8"?>'
'<svg xmlns="http://www.w3.org/2000/svg"', '<svg')
)
def _generate_secret_and_qr_code(username: str) -> Tuple[str, str, str]:
"""Generate a secret, url, and QR code."""
import pyotp
ota_secret = pyotp.random_base32()
url = pyotp.totp.TOTP(ota_secret).provisioning_uri(
username, issuer_name="Home Assistant")
image = _generate_qr_code(url)
return ota_secret, url, image
@MULTI_FACTOR_AUTH_MODULES.register('totp')
class TotpAuthModule(MultiFactorAuthModule):
"""Auth module validate time-based one time password."""
DEFAULT_TITLE = 'Time-based One Time Password'
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
"""Initialize the user data store."""
super().__init__(hass, config)
self._users = None # type: Optional[Dict[str, str]]
self._user_store = hass.helpers.storage.Store(
STORAGE_VERSION, STORAGE_KEY)
@property
def input_schema(self) -> vol.Schema:
"""Validate login flow input data."""
return vol.Schema({INPUT_FIELD_CODE: str})
async def _async_load(self) -> None:
"""Load stored data."""
data = await self._user_store.async_load()
if data is None:
data = {STORAGE_USERS: {}}
self._users = data.get(STORAGE_USERS, {})
async def _async_save(self) -> None:
"""Save data."""
await self._user_store.async_save({STORAGE_USERS: self._users})
def _add_ota_secret(self, user_id: str,
secret: Optional[str] = None) -> str:
"""Create a ota_secret for user."""
import pyotp
ota_secret = secret or pyotp.random_base32() # type: str
self._users[user_id] = ota_secret # type: ignore
return ota_secret
async def async_setup_flow(self, user_id: str) -> SetupFlow:
"""Return a data entry flow handler for setup module.
Mfa module should extend SetupFlow
"""
user = await self.hass.auth.async_get_user(user_id) # type: ignore
return TotpSetupFlow(self, self.input_schema, user)
async def async_setup_user(self, user_id: str, setup_data: Any) -> str:
"""Set up auth module for user."""
if self._users is None:
await self._async_load()
result = await self.hass.async_add_executor_job(
self._add_ota_secret, user_id, setup_data.get('secret'))
await self._async_save()
return result
async def async_depose_user(self, user_id: str) -> None:
"""Depose auth module for user."""
if self._users is None:
await self._async_load()
if self._users.pop(user_id, None): # type: ignore
await self._async_save()
async def async_is_user_setup(self, user_id: str) -> bool:
"""Return whether user is setup."""
if self._users is None:
await self._async_load()
return user_id in self._users # type: ignore
async def async_validation(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
if self._users is None:
await self._async_load()
# user_input has been validate in caller
# set INPUT_FIELD_CODE as vol.Required is not user friendly
return await self.hass.async_add_executor_job(
self._validate_2fa, user_id, user_input.get(INPUT_FIELD_CODE, ''))
def _validate_2fa(self, user_id: str, code: str) -> bool:
"""Validate two factor authentication code."""
import pyotp
ota_secret = self._users.get(user_id) # type: ignore
if ota_secret is None:
# even we cannot find user, we still do verify
# to make timing the same as if user was found.
pyotp.TOTP(DUMMY_SECRET).verify(code)
return False
return bool(pyotp.TOTP(ota_secret).verify(code))
class TotpSetupFlow(SetupFlow):
"""Handler for the setup flow."""
def __init__(self, auth_module: TotpAuthModule,
setup_schema: vol.Schema,
user: User) -> None:
"""Initialize the setup flow."""
super().__init__(auth_module, setup_schema, user.id)
# to fix typing complaint
self._auth_module = auth_module # type: TotpAuthModule
self._user = user
self._ota_secret = None # type: Optional[str]
self._url = None # type Optional[str]
self._image = None # type Optional[str]
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the first step of setup flow.
Return self.async_show_form(step_id='init') if user_input == None.
Return self.async_create_entry(data={'result': result}) if finish.
"""
import pyotp
errors = {} # type: Dict[str, str]
if user_input:
verified = await self.hass.async_add_executor_job( # type: ignore
pyotp.TOTP(self._ota_secret).verify, user_input['code'])
if verified:
result = await self._auth_module.async_setup_user(
self._user_id, {'secret': self._ota_secret})
return self.async_create_entry(
title=self._auth_module.name,
data={'result': result}
)
errors['base'] = 'invalid_code'
else:
hass = self._auth_module.hass
self._ota_secret, self._url, self._image = \
await hass.async_add_executor_job( # type: ignore
_generate_secret_and_qr_code, str(self._user.name))
return self.async_show_form(
step_id='init',
data_schema=self._setup_schema,
description_placeholders={
'code': self._ota_secret,
'url': self._url,
'qr_code': self._image
},
errors=errors
)

View File

@@ -1,5 +1,6 @@
"""Auth models."""
from datetime import datetime, timedelta
from typing import Dict, List, NamedTuple, Optional # noqa: F401
import uuid
import attr
@@ -14,17 +15,21 @@ from .util import generate_secret
class User:
"""A user."""
name = attr.ib(type=str)
name = attr.ib(type=str) # type: Optional[str]
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
is_owner = attr.ib(type=bool, default=False)
is_active = attr.ib(type=bool, default=False)
system_generated = attr.ib(type=bool, default=False)
# List of credentials of a user.
credentials = attr.ib(type=list, default=attr.Factory(list), cmp=False)
credentials = attr.ib(
type=list, default=attr.Factory(list), cmp=False
) # type: List[Credentials]
# Tokens associated with a user.
refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict), cmp=False)
refresh_tokens = attr.ib(
type=dict, default=attr.Factory(dict), cmp=False
) # type: Dict[str, RefreshToken]
@attr.s(slots=True)
@@ -32,33 +37,15 @@ class RefreshToken:
"""RefreshToken for a user to grant new access tokens."""
user = attr.ib(type=User)
client_id = attr.ib(type=str)
client_id = attr.ib(type=str) # type: Optional[str]
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
access_token_expiration = attr.ib(type=timedelta,
default=ACCESS_TOKEN_EXPIRATION)
token = attr.ib(type=str,
default=attr.Factory(lambda: generate_secret(64)))
access_tokens = attr.ib(type=list, default=attr.Factory(list), cmp=False)
@attr.s(slots=True)
class AccessToken:
"""Access token to access the API.
These will only ever be stored in memory and not be persisted.
"""
refresh_token = attr.ib(type=RefreshToken)
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
token = attr.ib(type=str,
default=attr.Factory(generate_secret))
@property
def expired(self):
"""Return if this token has expired."""
expires = self.created_at + self.refresh_token.access_token_expiration
return dt_util.utcnow() > expires
jwt_key = attr.ib(type=str,
default=attr.Factory(lambda: generate_secret(64)))
@attr.s(slots=True)
@@ -66,10 +53,14 @@ class Credentials:
"""Credentials for a user on an auth provider."""
auth_provider_type = attr.ib(type=str)
auth_provider_id = attr.ib(type=str)
auth_provider_id = attr.ib(type=str) # type: Optional[str]
# Allow the auth provider to store data to represent their auth.
data = attr.ib(type=dict)
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
is_new = attr.ib(type=bool, default=True)
UserMeta = NamedTuple("UserMeta",
[('name', Optional[str]), ('is_active', bool)])

View File

@@ -1,16 +1,22 @@
"""Auth providers for Home Assistant."""
import importlib
import logging
import types
from typing import Any, Dict, List, Optional
import voluptuous as vol
from voluptuous.humanize import humanize_error
from homeassistant import requirements
from homeassistant.core import callback
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID
from homeassistant import data_entry_flow, requirements
from homeassistant.core import callback, HomeAssistant
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import dt as dt_util
from homeassistant.util.decorator import Registry
from homeassistant.auth.models import Credentials
from ..auth_store import AuthStore
from ..models import Credentials, User, UserMeta # noqa: F401
from ..mfa_modules import SESSION_EXPIRATION
_LOGGER = logging.getLogger(__name__)
DATA_REQS = 'auth_prov_reqs_processed'
@@ -25,66 +31,20 @@ AUTH_PROVIDER_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
async def auth_provider_from_config(hass, store, config):
"""Initialize an auth provider from a config."""
provider_name = config[CONF_TYPE]
module = await load_auth_provider_module(hass, provider_name)
if module is None:
return None
try:
config = module.CONFIG_SCHEMA(config)
except vol.Invalid as err:
_LOGGER.error('Invalid configuration for auth provider %s: %s',
provider_name, humanize_error(config, err))
return None
return AUTH_PROVIDERS[provider_name](hass, store, config)
async def load_auth_provider_module(hass, provider):
"""Load an auth provider."""
try:
module = importlib.import_module(
'homeassistant.auth.providers.{}'.format(provider))
except ImportError:
_LOGGER.warning('Unable to find auth provider %s', provider)
return None
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
return module
processed = hass.data.get(DATA_REQS)
if processed is None:
processed = hass.data[DATA_REQS] = set()
elif provider in processed:
return module
req_success = await requirements.async_process_requirements(
hass, 'auth provider {}'.format(provider), module.REQUIREMENTS)
if not req_success:
return None
processed.add(provider)
return module
class AuthProvider:
"""Provider of user authentication."""
DEFAULT_TITLE = 'Unnamed auth provider'
def __init__(self, hass, store, config):
def __init__(self, hass: HomeAssistant, store: AuthStore,
config: Dict[str, Any]) -> None:
"""Initialize an auth provider."""
self.hass = hass
self.store = store
self.config = config
@property
def id(self): # pylint: disable=invalid-name
def id(self) -> Optional[str]: # pylint: disable=invalid-name
"""Return id of the auth provider.
Optional, can be None.
@@ -92,16 +52,21 @@ class AuthProvider:
return self.config.get(CONF_ID)
@property
def type(self):
def type(self) -> str:
"""Return type of the provider."""
return self.config[CONF_TYPE]
return self.config[CONF_TYPE] # type: ignore
@property
def name(self):
def name(self) -> str:
"""Return the name of the auth provider."""
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
async def async_credentials(self):
@property
def support_mfa(self) -> bool:
"""Return whether multi-factor auth supported by the auth provider."""
return True
async def async_credentials(self) -> List[Credentials]:
"""Return all credentials of this provider."""
users = await self.store.async_get_users()
return [
@@ -113,7 +78,7 @@ class AuthProvider:
]
@callback
def async_create_credentials(self, data):
def async_create_credentials(self, data: Dict[str, str]) -> Credentials:
"""Create credentials."""
return Credentials(
auth_provider_type=self.type,
@@ -123,21 +88,169 @@ class AuthProvider:
# Implement by extending class
async def async_credential_flow(self):
"""Return the data flow for logging in with auth provider."""
async def async_login_flow(self, context: Optional[Dict]) -> 'LoginFlow':
"""Return the data flow for logging in with auth provider.
Auth provider should extend LoginFlow and return an instance.
"""
raise NotImplementedError
async def async_get_or_create_credentials(self, flow_result):
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
"""Get credentials based on the flow result."""
raise NotImplementedError
async def async_user_meta_for_credentials(self, credentials):
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
"""Return extra user metadata for credentials.
Will be used to populate info when creating a new user.
Values to populate:
- name: string
- is_active: boolean
"""
return {}
raise NotImplementedError
async def auth_provider_from_config(
hass: HomeAssistant, store: AuthStore,
config: Dict[str, Any]) -> AuthProvider:
"""Initialize an auth provider from a config."""
provider_name = config[CONF_TYPE]
module = await load_auth_provider_module(hass, provider_name)
try:
config = module.CONFIG_SCHEMA(config) # type: ignore
except vol.Invalid as err:
_LOGGER.error('Invalid configuration for auth provider %s: %s',
provider_name, humanize_error(config, err))
raise
return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore
async def load_auth_provider_module(
hass: HomeAssistant, provider: str) -> types.ModuleType:
"""Load an auth provider."""
try:
module = importlib.import_module(
'homeassistant.auth.providers.{}'.format(provider))
except ImportError as err:
_LOGGER.error('Unable to load auth provider %s: %s', provider, err)
raise HomeAssistantError('Unable to load auth provider {}: {}'.format(
provider, err))
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
return module
processed = hass.data.get(DATA_REQS)
if processed is None:
processed = hass.data[DATA_REQS] = set()
elif provider in processed:
return module
# https://github.com/python/mypy/issues/1424
reqs = module.REQUIREMENTS # type: ignore
req_success = await requirements.async_process_requirements(
hass, 'auth provider {}'.format(provider), reqs)
if not req_success:
raise HomeAssistantError(
'Unable to process requirements of auth provider {}'.format(
provider))
processed.add(provider)
return module
class LoginFlow(data_entry_flow.FlowHandler):
"""Handler for the login flow."""
def __init__(self, auth_provider: AuthProvider) -> None:
"""Initialize the login flow."""
self._auth_provider = auth_provider
self._auth_module_id = None # type: Optional[str]
self._auth_manager = auth_provider.hass.auth # type: ignore
self.available_mfa_modules = {} # type: Dict[str, str]
self.created_at = dt_util.utcnow()
self.user = None # type: Optional[User]
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the first step of login flow.
Return self.async_show_form(step_id='init') if user_input == None.
Return await self.async_finish(flow_result) if login init step pass.
"""
raise NotImplementedError
async def async_step_select_mfa_module(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the step of select mfa module."""
errors = {}
if user_input is not None:
auth_module = user_input.get('multi_factor_auth_module')
if auth_module in self.available_mfa_modules:
self._auth_module_id = auth_module
return await self.async_step_mfa()
errors['base'] = 'invalid_auth_module'
if len(self.available_mfa_modules) == 1:
self._auth_module_id = list(self.available_mfa_modules.keys())[0]
return await self.async_step_mfa()
return self.async_show_form(
step_id='select_mfa_module',
data_schema=vol.Schema({
'multi_factor_auth_module': vol.In(self.available_mfa_modules)
}),
errors=errors,
)
async def async_step_mfa(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the step of mfa validation."""
errors = {}
auth_module = self._auth_manager.get_auth_mfa_module(
self._auth_module_id)
if auth_module is None:
# Given an invalid input to async_step_select_mfa_module
# will show invalid_auth_module error
return await self.async_step_select_mfa_module(user_input={})
if user_input is not None:
expires = self.created_at + SESSION_EXPIRATION
if dt_util.utcnow() > expires:
return self.async_abort(
reason='login_expired'
)
result = await auth_module.async_validation(
self.user.id, user_input) # type: ignore
if not result:
errors['base'] = 'invalid_code'
if not errors:
return await self.async_finish(self.user)
description_placeholders = {
'mfa_module_name': auth_module.name,
'mfa_module_id': auth_module.id
} # type: Dict[str, str]
return self.async_show_form(
step_id='mfa',
data_schema=auth_module.input_schema,
description_placeholders=description_placeholders,
errors=errors,
)
async def async_finish(self, flow_result: Any) -> Dict:
"""Handle the pass of login flow."""
return self.async_create_entry(
title=self._auth_provider.name,
data=flow_result
)

View File

@@ -3,24 +3,27 @@ import base64
from collections import OrderedDict
import hashlib
import hmac
from typing import Dict # noqa: F401 pylint: disable=unused-import
from typing import Any, Dict, List, Optional, cast
import bcrypt
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.const import CONF_ID
from homeassistant.core import callback
from homeassistant.core import callback, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.async_ import run_coroutine_threadsafe
from homeassistant.auth.util import generate_secret
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from ..models import Credentials, UserMeta
from ..util import generate_secret
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
STORAGE_VERSION = 1
STORAGE_KEY = 'auth_provider.homeassistant'
def _disallow_id(conf):
def _disallow_id(conf: Dict[str, Any]) -> Dict[str, Any]:
"""Disallow ID in config."""
if CONF_ID in conf:
raise vol.Invalid(
@@ -46,13 +49,13 @@ class InvalidUser(HomeAssistantError):
class Data:
"""Hold the user data."""
def __init__(self, hass):
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the user data store."""
self.hass = hass
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
self._data = None
self._data = None # type: Optional[Dict[str, Any]]
async def async_load(self):
async def async_load(self) -> None:
"""Load stored data."""
data = await self._store.async_load()
@@ -65,37 +68,69 @@ class Data:
self._data = data
@property
def users(self):
def users(self) -> List[Dict[str, str]]:
"""Return users."""
return self._data['users']
return self._data['users'] # type: ignore
def validate_login(self, username: str, password: str) -> None:
"""Validate a username and password.
Raises InvalidAuth if auth invalid.
"""
hashed = self.hash_password(password)
dummy = b'$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO'
found = None
# Compare all users to avoid timing attacks.
for user in self._data['users']:
for user in self.users:
if username == user['username']:
found = user
if found is None:
# Do one more compare to make timing the same as if user was found.
hmac.compare_digest(hashed, hashed)
# check a hash to make timing the same as if user was found
bcrypt.checkpw(b'foo',
dummy)
raise InvalidAuth
if not hmac.compare_digest(hashed,
base64.b64decode(found['password'])):
user_hash = base64.b64decode(found['password'])
# if the hash is not a bcrypt hash...
# provide a transparant upgrade for old pbkdf2 hash format
if not (user_hash.startswith(b'$2a$')
or user_hash.startswith(b'$2b$')
or user_hash.startswith(b'$2x$')
or user_hash.startswith(b'$2y$')):
# IMPORTANT! validate the login, bail if invalid
hashed = self.legacy_hash_password(password)
if not hmac.compare_digest(hashed, user_hash):
raise InvalidAuth
# then re-hash the valid password with bcrypt
self.change_password(found['username'], password)
run_coroutine_threadsafe(
self.async_save(), self.hass.loop
).result()
user_hash = base64.b64decode(found['password'])
# bcrypt.checkpw is timing-safe
if not bcrypt.checkpw(password.encode(),
user_hash):
raise InvalidAuth
def legacy_hash_password(self, password: str,
for_storage: bool = False) -> bytes:
"""LEGACY password encoding."""
# We're no longer storing salts in data, but if one exists we
# should be able to retrieve it.
salt = self._data['salt'].encode() # type: ignore
hashed = hashlib.pbkdf2_hmac('sha512', password.encode(), salt, 100000)
if for_storage:
hashed = base64.b64encode(hashed)
return hashed
# pylint: disable=no-self-use
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
"""Encode a password."""
hashed = hashlib.pbkdf2_hmac(
'sha512', password.encode(), self._data['salt'].encode(), 100000)
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)) \
# type: bytes
if for_storage:
hashed = base64.b64encode(hashed)
return hashed
@@ -137,7 +172,7 @@ class Data:
else:
raise InvalidUser
async def async_save(self):
async def async_save(self) -> None:
"""Save data."""
await self._store.async_save(self._data)
@@ -150,7 +185,7 @@ class HassAuthProvider(AuthProvider):
data = None
async def async_initialize(self):
async def async_initialize(self) -> None:
"""Initialize the auth provider."""
if self.data is not None:
return
@@ -158,19 +193,22 @@ class HassAuthProvider(AuthProvider):
self.data = Data(self.hass)
await self.data.async_load()
async def async_credential_flow(self):
async def async_login_flow(
self, context: Optional[Dict]) -> LoginFlow:
"""Return a flow to login."""
return LoginFlow(self)
return HassLoginFlow(self)
async def async_validate_login(self, username: str, password: str):
"""Helper to validate a username and password."""
async def async_validate_login(self, username: str, password: str) -> None:
"""Validate a username and password."""
if self.data is None:
await self.async_initialize()
assert self.data is not None
await self.hass.async_add_executor_job(
self.data.validate_login, username, password)
async def async_get_or_create_credentials(self, flow_result):
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
"""Get credentials based on the flow result."""
username = flow_result['username']
@@ -183,17 +221,17 @@ class HassAuthProvider(AuthProvider):
'username': username
})
async def async_user_meta_for_credentials(self, credentials):
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
"""Get extra info for this credential."""
return {
'name': credentials.data['username'],
'is_active': True,
}
return UserMeta(name=credentials.data['username'], is_active=True)
async def async_will_remove_credentials(self, credentials):
async def async_will_remove_credentials(
self, credentials: Credentials) -> None:
"""When credentials get removed, also remove the auth."""
if self.data is None:
await self.async_initialize()
assert self.data is not None
try:
self.data.async_remove_auth(credentials.data['username'])
@@ -203,29 +241,26 @@ class HassAuthProvider(AuthProvider):
pass
class LoginFlow(data_entry_flow.FlowHandler):
class HassLoginFlow(LoginFlow):
"""Handler for the login flow."""
def __init__(self, auth_provider):
"""Initialize the login flow."""
self._auth_provider = auth_provider
async def async_step_init(self, user_input=None):
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the step of the form."""
errors = {}
if user_input is not None:
try:
await self._auth_provider.async_validate_login(
user_input['username'], user_input['password'])
await cast(HassAuthProvider, self._auth_provider)\
.async_validate_login(user_input['username'],
user_input['password'])
except InvalidAuth:
errors['base'] = 'invalid_auth'
if not errors:
return self.async_create_entry(
title=self._auth_provider.name,
data=user_input
)
user_input.pop('password')
return await self.async_finish(user_input)
schema = OrderedDict() # type: Dict[str, type]
schema['username'] = str

View File

@@ -1,14 +1,15 @@
"""Example auth provider."""
from collections import OrderedDict
import hmac
from typing import Any, Dict, Optional, cast
import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError
from homeassistant import data_entry_flow
from homeassistant.core import callback
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from ..models import Credentials, UserMeta
USER_SCHEMA = vol.Schema({
@@ -31,13 +32,13 @@ class InvalidAuthError(HomeAssistantError):
class ExampleAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords."""
async def async_credential_flow(self):
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
"""Return a flow to login."""
return LoginFlow(self)
return ExampleLoginFlow(self)
@callback
def async_validate_login(self, username, password):
"""Helper to validate a username and password."""
def async_validate_login(self, username: str, password: str) -> None:
"""Validate a username and password."""
user = None
# Compare all users to avoid timing attacks.
@@ -56,7 +57,8 @@ class ExampleAuthProvider(AuthProvider):
password.encode('utf-8')):
raise InvalidAuthError
async def async_get_or_create_credentials(self, flow_result):
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
"""Get credentials based on the flow result."""
username = flow_result['username']
@@ -69,49 +71,45 @@ class ExampleAuthProvider(AuthProvider):
'username': username
})
async def async_user_meta_for_credentials(self, credentials):
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
"""Return extra user metadata for credentials.
Will be used to populate info when creating a new user.
"""
username = credentials.data['username']
info = {
'is_active': True,
}
name = None
for user in self.config['users']:
if user['username'] == username:
info['name'] = user.get('name')
name = user.get('name')
break
return info
return UserMeta(name=name, is_active=True)
class LoginFlow(data_entry_flow.FlowHandler):
class ExampleLoginFlow(LoginFlow):
"""Handler for the login flow."""
def __init__(self, auth_provider):
"""Initialize the login flow."""
self._auth_provider = auth_provider
async def async_step_init(self, user_input=None):
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the step of the form."""
errors = {}
if user_input is not None:
try:
self._auth_provider.async_validate_login(
user_input['username'], user_input['password'])
cast(ExampleAuthProvider, self._auth_provider)\
.async_validate_login(user_input['username'],
user_input['password'])
except InvalidAuthError:
errors['base'] = 'invalid_auth'
if not errors:
return self.async_create_entry(
title=self._auth_provider.name,
data=user_input
)
user_input.pop('password')
return await self.async_finish(user_input)
schema = OrderedDict()
schema = OrderedDict() # type: Dict[str, type]
schema['username'] = str
schema['password'] = str

View File

@@ -3,16 +3,17 @@ Support Legacy API password auth provider.
It will be removed when auth system production ready
"""
from collections import OrderedDict
import hmac
from typing import Any, Dict, Optional, cast
import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError
from homeassistant import data_entry_flow
from homeassistant.components.http import HomeAssistantHTTP # noqa: F401
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from ..models import Credentials, UserMeta
USER_SCHEMA = vol.Schema({
@@ -36,25 +37,21 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
DEFAULT_TITLE = 'Legacy API Password'
async def async_credential_flow(self):
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
"""Return a flow to login."""
return LoginFlow(self)
return LegacyLoginFlow(self)
@callback
def async_validate_login(self, password):
"""Helper to validate a username and password."""
if not hasattr(self.hass, 'http'):
raise ValueError('http component is not loaded')
def async_validate_login(self, password: str) -> None:
"""Validate a username and password."""
hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
if self.hass.http.api_password is None:
raise ValueError('http component is not configured using'
' api_password')
if not hmac.compare_digest(self.hass.http.api_password.encode('utf-8'),
if not hmac.compare_digest(hass_http.api_password.encode('utf-8'),
password.encode('utf-8')):
raise InvalidAuthError
async def async_get_or_create_credentials(self, flow_result):
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
"""Return LEGACY_USER always."""
for credential in await self.async_credentials():
if credential.data['username'] == LEGACY_USER:
@@ -64,47 +61,43 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
'username': LEGACY_USER
})
async def async_user_meta_for_credentials(self, credentials):
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
"""
Set name as LEGACY_USER always.
Will be used to populate info when creating a new user.
"""
return {
'name': LEGACY_USER,
'is_active': True,
}
return UserMeta(name=LEGACY_USER, is_active=True)
class LoginFlow(data_entry_flow.FlowHandler):
class LegacyLoginFlow(LoginFlow):
"""Handler for the login flow."""
def __init__(self, auth_provider):
"""Initialize the login flow."""
self._auth_provider = auth_provider
async def async_step_init(self, user_input=None):
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the step of the form."""
errors = {}
hass_http = getattr(self.hass, 'http', None)
if hass_http is None or not hass_http.api_password:
return self.async_abort(
reason='no_api_password_set'
)
if user_input is not None:
try:
self._auth_provider.async_validate_login(
user_input['password'])
cast(LegacyApiPasswordAuthProvider, self._auth_provider)\
.async_validate_login(user_input['password'])
except InvalidAuthError:
errors['base'] = 'invalid_auth'
if not errors:
return self.async_create_entry(
title=self._auth_provider.name,
data={}
)
schema = OrderedDict()
schema['password'] = str
return await self.async_finish({})
return self.async_show_form(
step_id='init',
data_schema=vol.Schema(schema),
data_schema=vol.Schema({'password': str}),
errors=errors,
)

View File

@@ -0,0 +1,129 @@
"""Trusted Networks auth provider.
It shows list of users if access from trusted network.
Abort login flow if not access from trusted network.
"""
from typing import Any, Dict, Optional, cast
import voluptuous as vol
from homeassistant.components.http import HomeAssistantHTTP # noqa: F401
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from ..models import Credentials, UserMeta
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA)
class InvalidAuthError(HomeAssistantError):
"""Raised when try to access from untrusted networks."""
class InvalidUserError(HomeAssistantError):
"""Raised when try to login as invalid user."""
@AUTH_PROVIDERS.register('trusted_networks')
class TrustedNetworksAuthProvider(AuthProvider):
"""Trusted Networks auth provider.
Allow passwordless access from trusted network.
"""
DEFAULT_TITLE = 'Trusted Networks'
@property
def support_mfa(self) -> bool:
"""Trusted Networks auth provider does not support MFA."""
return False
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
"""Return a flow to login."""
assert context is not None
users = await self.store.async_get_users()
available_users = {user.id: user.name
for user in users
if not user.system_generated and user.is_active}
return TrustedNetworksLoginFlow(
self, cast(str, context.get('ip_address')), available_users)
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
"""Get credentials based on the flow result."""
user_id = flow_result['user']
users = await self.store.async_get_users()
for user in users:
if (not user.system_generated and
user.is_active and
user.id == user_id):
for credential in await self.async_credentials():
if credential.data['user_id'] == user_id:
return credential
cred = self.async_create_credentials({'user_id': user_id})
await self.store.async_link_user(user, cred)
return cred
# We only allow login as exist user
raise InvalidUserError
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
"""Return extra user metadata for credentials.
Trusted network auth provider should never create new user.
"""
raise NotImplementedError
@callback
def async_validate_access(self, ip_address: str) -> None:
"""Make sure the access from trusted networks.
Raise InvalidAuthError if not.
Raise InvalidAuthError if trusted_networks is not configured.
"""
hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
if not hass_http or not hass_http.trusted_networks:
raise InvalidAuthError('trusted_networks is not configured')
if not any(ip_address in trusted_network for trusted_network
in hass_http.trusted_networks):
raise InvalidAuthError('Not in trusted_networks')
class TrustedNetworksLoginFlow(LoginFlow):
"""Handler for the login flow."""
def __init__(self, auth_provider: TrustedNetworksAuthProvider,
ip_address: str, available_users: Dict[str, Optional[str]]) \
-> None:
"""Initialize the login flow."""
super().__init__(auth_provider)
self._available_users = available_users
self._ip_address = ip_address
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the step of the form."""
try:
cast(TrustedNetworksAuthProvider, self._auth_provider)\
.async_validate_access(self._ip_address)
except InvalidAuthError:
return self.async_abort(
reason='not_whitelisted'
)
if user_input is not None:
return await self.async_finish(user_input)
return self.async_show_form(
step_id='init',
data_schema=vol.Schema({'user': vol.In(self._available_users)}),
)

View File

@@ -61,7 +61,6 @@ def from_config_dict(config: Dict[str, Any],
config, hass, config_dir, enable_log, verbose, skip_pip,
log_rotate_days, log_file, log_no_color)
)
return hass
@@ -87,11 +86,20 @@ async def async_from_config_dict(config: Dict[str, Any],
log_no_color)
core_config = config.get(core.DOMAIN, {})
has_api_password = bool((config.get('http') or {}).get('api_password'))
has_trusted_networks = bool((config.get('http') or {})
.get('trusted_networks'))
try:
await conf_util.async_process_ha_core_config(hass, core_config)
except vol.Invalid as ex:
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
await conf_util.async_process_ha_core_config(
hass, core_config, has_api_password, has_trusted_networks)
except vol.Invalid as config_err:
conf_util.async_log_exception(
config_err, 'homeassistant', core_config, hass)
return None
except HomeAssistantError:
_LOGGER.error("Home Assistant core failed to initialize. "
"Further initialization aborted")
return None
await hass.async_add_executor_job(
@@ -126,7 +134,7 @@ async def async_from_config_dict(config: Dict[str, Any],
res = await core_components.async_setup(hass, config)
if not res:
_LOGGER.error("Home Assistant core failed to initialize. "
"further initialization aborted")
"Further initialization aborted")
return hass
await persistent_notification.async_setup(hass, config)
@@ -307,7 +315,7 @@ def async_enable_logging(hass: core.HomeAssistant,
hass.data[DATA_LOGGING] = err_log_path
else:
_LOGGER.error(
"Unable to setup error log %s (access denied)", err_log_path)
"Unable to set up error log %s (access denied)", err_log_path)
async def async_mount_local_lib_path(config_dir: str) -> str:

View File

@@ -10,6 +10,7 @@ Component design guidelines:
import asyncio
import itertools as it
import logging
from typing import Awaitable
import homeassistant.core as ha
import homeassistant.config as conf_util
@@ -109,7 +110,7 @@ def async_reload_core_config(hass):
@asyncio.coroutine
def async_setup(hass, config):
def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
"""Set up general services related to Home Assistant."""
@asyncio.coroutine
def async_handle_turn_service(service):

View File

@@ -26,20 +26,6 @@ ATTR_CHANGED_BY = 'changed_by'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
SERVICE_TO_METHOD = {
SERVICE_ALARM_DISARM: 'alarm_disarm',
SERVICE_ALARM_ARM_HOME: 'alarm_arm_home',
SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away',
SERVICE_ALARM_ARM_NIGHT: 'alarm_arm_night',
SERVICE_ALARM_ARM_CUSTOM_BYPASS: 'alarm_arm_custom_bypass',
SERVICE_ALARM_TRIGGER: 'alarm_trigger'
}
ATTR_TO_PROPERTY = [
ATTR_CODE,
ATTR_CODE_FORMAT
]
ALARM_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_CODE): cv.string,
@@ -126,36 +112,36 @@ def async_setup(hass, config):
yield from component.async_setup(config)
@asyncio.coroutine
def async_alarm_service_handler(service):
"""Map services to methods on Alarm."""
target_alarms = component.async_extract_from_service(service)
code = service.data.get(ATTR_CODE)
method = "async_{}".format(SERVICE_TO_METHOD[service.service])
update_tasks = []
for alarm in target_alarms:
yield from getattr(alarm, method)(code)
if not alarm.should_poll:
continue
update_tasks.append(alarm.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
for service in SERVICE_TO_METHOD:
hass.services.async_register(
DOMAIN, service, async_alarm_service_handler,
schema=ALARM_SERVICE_SCHEMA)
component.async_register_entity_service(
SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA,
'async_alarm_disarm'
)
component.async_register_entity_service(
SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA,
'async_alarm_arm_home'
)
component.async_register_entity_service(
SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA,
'async_alarm_arm_away'
)
component.async_register_entity_service(
SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA,
'async_alarm_arm_night'
)
component.async_register_entity_service(
SERVICE_ALARM_ARM_CUSTOM_BYPASS, ALARM_SERVICE_SCHEMA,
'async_alarm_arm_custom_bypass'
)
component.async_register_entity_service(
SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA,
'async_alarm_trigger'
)
return True
async def async_setup_entry(hass, entry):
"""Setup a config entry."""
"""Set up a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)

View File

@@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__)
ICON = 'mdi:security'
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up an alarm control panel for an Abode device."""
data = hass.data[ABODE_DOMAIN]
@@ -28,7 +28,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
data.devices.extend(alarm_devices)
add_devices(alarm_devices)
add_entities(alarm_devices)
class AbodeAlarm(AbodeDevice, AlarmControlPanel):

View File

@@ -26,10 +26,10 @@ ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up for AlarmDecoder alarm panels."""
device = AlarmDecoderAlarmPanel()
add_devices([device])
add_entities([device])
def alarm_toggle_chime_handler(service):
"""Register toggle chime handler."""

View File

@@ -33,7 +33,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up a Alarm.com control panel."""
name = config.get(CONF_NAME)
code = config.get(CONF_CODE)
@@ -42,7 +43,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
alarmdotcom = AlarmDotCom(hass, name, code, username, password)
yield from alarmdotcom.async_login()
async_add_devices([alarmdotcom])
async_add_entities([alarmdotcom])
class AlarmDotCom(alarm.AlarmControlPanel):

View File

@@ -38,7 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Arlo Alarm Control Panels."""
arlo = hass.data[DATA_ARLO]
@@ -51,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for base_station in arlo.base_stations:
base_stations.append(ArloBaseStation(base_station, home_mode_name,
away_mode_name))
add_devices(base_stations, True)
add_entities(base_stations, True)
class ArloBaseStation(AlarmControlPanel):

View File

@@ -16,7 +16,7 @@ DEPENDENCIES = ['canary']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Canary alarms."""
data = hass.data[DATA_CANARY]
devices = []
@@ -24,7 +24,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for location in data.locations:
devices.append(CanaryAlarm(data, location.location_id))
add_devices(devices, True)
add_entities(devices, True)
class CanaryAlarm(AlarmControlPanel):

View File

@@ -35,7 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Concord232 alarm control panel platform."""
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
@@ -44,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
url = 'http://{}:{}'.format(host, port)
try:
add_devices([Concord232Alarm(hass, url, name)])
add_entities([Concord232Alarm(hass, url, name)])
except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
return

View File

@@ -13,9 +13,9 @@ from homeassistant.const import (
CONF_PENDING_TIME, CONF_TRIGGER_TIME)
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Demo alarm control panel platform."""
add_devices([
add_entities([
manual.ManualAlarm(hass, 'Alarm', '1234', None, False, {
STATE_ALARM_ARMED_AWAY: {
CONF_DELAY_TIME: datetime.timedelta(seconds=0),

View File

@@ -34,7 +34,7 @@ STATES = {
}
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Egardia platform."""
if discovery_info is None:
return
@@ -45,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
discovery_info.get(CONF_REPORT_SERVER_CODES),
discovery_info[CONF_REPORT_SERVER_PORT])
# add egardia alarm device
add_devices([device], True)
add_entities([device], True)
class EgardiaAlarm(alarm.AlarmControlPanel):

View File

@@ -33,7 +33,8 @@ ALARM_KEYPRESS_SCHEMA = vol.Schema({
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Perform the setup for Envisalink alarm panels."""
configured_partitions = discovery_info['partitions']
code = discovery_info[CONF_CODE]
@@ -53,7 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
)
devices.append(device)
async_add_devices(devices)
async_add_entities(devices)
@callback
def alarm_keypress_handler(service):

View File

@@ -1,36 +1,35 @@
"""
Support for HomematicIP alarm control panel.
Support for HomematicIP Cloud alarm control panel.
For more details about this component, please refer to the documentation at
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.homematicip_cloud/
"""
import logging
from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.components.homematicip_cloud import (
HMIPC_HAPID, HomematicipGenericDevice)
from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED)
from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.components.homematicip_cloud import (
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
HMIPC_HAPID)
DEPENDENCIES = ['homematicip_cloud']
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['homematicip_cloud']
HMIP_ZONE_AWAY = 'EXTERNAL'
HMIP_ZONE_HOME = 'INTERNAL'
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the HomematicIP alarm control devices."""
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Set up the HomematicIP Cloud alarm control devices."""
pass
async def async_setup_entry(hass, config_entry, async_add_devices):
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the HomematicIP alarm control panel from a config entry."""
from homematicip.aio.group import AsyncSecurityZoneGroup
@@ -41,11 +40,11 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
devices.append(HomematicipSecurityZone(home, group))
if devices:
async_add_devices(devices)
async_add_entities(devices)
class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
"""Representation of an HomematicIP security zone group."""
"""Representation of an HomematicIP Cloud security zone group."""
def __init__(self, home, device):
"""Initialize the security zone group."""

View File

@@ -40,7 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up an iAlarm control panel."""
name = config.get(CONF_NAME)
username = config.get(CONF_USERNAME)
@@ -49,7 +49,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
url = 'http://{}'.format(host)
ialarm = IAlarmPanel(name, username, password, url)
add_devices([ialarm], True)
add_entities([ialarm], True)
class IAlarmPanel(alarm.AlarmControlPanel):

View File

@@ -59,7 +59,7 @@ PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a control panel managed through IFTTT."""
if DATA_IFTTT_ALARM not in hass.data:
hass.data[DATA_IFTTT_ALARM] = []
@@ -75,7 +75,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
alarmpanel = IFTTTAlarmPanel(name, code, event_away, event_home,
event_night, event_disarm, optimistic)
hass.data[DATA_IFTTT_ALARM].append(alarmpanel)
add_devices([alarmpanel])
add_entities([alarmpanel])
async def push_state_update(service):
"""Set the service state as device state attribute."""

View File

@@ -103,9 +103,9 @@ PLATFORM_SCHEMA = vol.Schema(vol.All({
}, _state_validator))
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the manual alarm platform."""
add_devices([ManualAlarm(
add_entities([ManualAlarm(
hass,
config[CONF_NAME],
config.get(CONF_CODE),

View File

@@ -123,9 +123,9 @@ PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
}), _state_validator))
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the manual MQTT alarm platform."""
add_devices([ManualMQTTAlarm(
add_entities([ManualMQTTAlarm(
hass,
config[CONF_NAME],
config.get(CONF_CODE),

View File

@@ -47,12 +47,13 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the MQTT Alarm Control Panel platform."""
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
async_add_devices([MqttAlarm(
async_add_entities([MqttAlarm(
config.get(CONF_NAME),
config.get(CONF_STATE_TOPIC),
config.get(CONF_COMMAND_TOPIC),

View File

@@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the NX584 platform."""
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
@@ -40,7 +40,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
url = 'http://{}:{}'.format(host, port)
try:
add_devices([NX584Alarm(hass, url, name)])
add_entities([NX584Alarm(hass, url, name)])
except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to NX584: %s", str(ex))
return False

View File

@@ -19,14 +19,15 @@ DEPENDENCIES = ['satel_integra']
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up for Satel Integra alarm panels."""
if not discovery_info:
return
device = SatelIntegraAlarmPanel(
"Alarm Panel", discovery_info.get(CONF_ARM_HOME_MODE))
async_add_devices([device])
async_add_entities([device])
class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):

View File

@@ -34,7 +34,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the SimpliSafe platform."""
from simplipy.api import SimpliSafeApiInterface, SimpliSafeAPIException
name = config.get(CONF_NAME)
@@ -45,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
try:
simplisafe = SimpliSafeApiInterface(username, password)
except SimpliSafeAPIException:
_LOGGER.error("Failed to setup SimpliSafe")
_LOGGER.error("Failed to set up SimpliSafe")
return
systems = []
@@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for system in simplisafe.get_systems():
systems.append(SimpliSafeAlarm(system, name, code))
add_devices(systems)
add_entities(systems)
class SimpliSafeAlarm(AlarmControlPanel):

View File

@@ -29,7 +29,8 @@ def _get_alarm_state(spc_mode):
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the SPC alarm control panel platform."""
if (discovery_info is None or
discovery_info[ATTR_DISCOVER_AREAS] is None):
@@ -39,7 +40,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
devices = [SpcAlarm(api, area)
for area in discovery_info[ATTR_DISCOVER_AREAS]]
async_add_devices(devices)
async_add_entities(devices)
class SpcAlarm(alarm.AlarmControlPanel):

View File

@@ -31,14 +31,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a TotalConnect control panel."""
name = config.get(CONF_NAME)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
total_connect = TotalConnect(name, username, password)
add_devices([total_connect], True)
add_entities([total_connect], True)
class TotalConnect(alarm.AlarmControlPanel):

View File

@@ -17,13 +17,13 @@ from homeassistant.const import (
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Verisure platform."""
alarms = []
if int(hub.config.get(CONF_ALARM, 1)):
hub.update_overview()
alarms.append(VerisureAlarm())
add_devices(alarms)
add_entities(alarms)
def set_arm_state(state, code=None):

View File

@@ -20,7 +20,7 @@ DEPENDENCIES = ['wink']
STATE_ALARM_PRIVACY = 'Private'
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Wink platform."""
import pywink
@@ -32,7 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
except AttributeError:
_id = camera.object_id() + camera.name()
if _id not in hass.data[DOMAIN]['unique_ids']:
add_devices([WinkCameraDevice(camera, hass)])
add_entities([WinkCameraDevice(camera, hass)])
class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):

View File

@@ -111,6 +111,7 @@ def async_setup(hass, config):
for alert_id in alert_ids:
alert = all_alerts[alert_id]
alert.async_set_context(service_call.context)
if service_call.service == SERVICE_TURN_ON:
yield from alert.async_turn_on()
elif service_call.service == SERVICE_TOGGLE:

View File

@@ -13,12 +13,13 @@ import homeassistant.util.color as color_util
from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.util.decorator import Registry
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_NAME,
SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE,
ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, SERVICE_LOCK,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
@@ -53,6 +54,7 @@ CONF_DISPLAY_CATEGORIES = 'display_categories'
HANDLERS = Registry()
ENTITY_ADAPTERS = Registry()
EVENT_ALEXA_SMART_HOME = 'alexa_smart_home'
class _DisplayCategory:
@@ -159,7 +161,8 @@ class _AlexaEntity:
The API handlers should manipulate entities only through this interface.
"""
def __init__(self, config, entity):
def __init__(self, hass, config, entity):
self.hass = hass
self.config = config
self.entity = entity
self.entity_conf = config.entity_config.get(entity.entity_id, {})
@@ -383,6 +386,10 @@ class _AlexaInputController(_AlexaInterface):
class _AlexaTemperatureSensor(_AlexaInterface):
def __init__(self, hass, entity):
_AlexaInterface.__init__(self, entity)
self.hass = hass
def name(self):
return 'Alexa.TemperatureSensor'
@@ -396,9 +403,10 @@ class _AlexaTemperatureSensor(_AlexaInterface):
if name != 'temperature':
raise _UnsupportedProperty(name)
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
temp = self.entity.state
if self.entity.domain == climate.DOMAIN:
unit = self.hass.config.units.temperature_unit
temp = self.entity.attributes.get(
climate.ATTR_CURRENT_TEMPERATURE)
return {
@@ -408,6 +416,10 @@ class _AlexaTemperatureSensor(_AlexaInterface):
class _AlexaThermostatController(_AlexaInterface):
def __init__(self, hass, entity):
_AlexaInterface.__init__(self, entity)
self.hass = hass
def name(self):
return 'Alexa.ThermostatController'
@@ -438,8 +450,7 @@ class _AlexaThermostatController(_AlexaInterface):
raise _UnsupportedProperty(name)
return mode
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
temp = None
unit = self.hass.config.units.temperature_unit
if name == 'targetSetpoint':
temp = self.entity.attributes.get(climate.ATTR_TEMPERATURE)
elif name == 'lowerSetpoint':
@@ -490,8 +501,8 @@ class _ClimateCapabilities(_AlexaEntity):
return [_DisplayCategory.THERMOSTAT]
def interfaces(self):
yield _AlexaThermostatController(self.entity)
yield _AlexaTemperatureSensor(self.entity)
yield _AlexaThermostatController(self.hass, self.entity)
yield _AlexaTemperatureSensor(self.hass, self.entity)
@ENTITY_ADAPTERS.register(cover.DOMAIN)
@@ -608,11 +619,11 @@ class _SensorCapabilities(_AlexaEntity):
def interfaces(self):
attrs = self.entity.attributes
if attrs.get(CONF_UNIT_OF_MEASUREMENT) in (
if attrs.get(ATTR_UNIT_OF_MEASUREMENT) in (
TEMP_FAHRENHEIT,
TEMP_CELSIUS,
):
yield _AlexaTemperatureSensor(self.entity)
yield _AlexaTemperatureSensor(self.hass, self.entity)
class _Cause:
@@ -703,24 +714,47 @@ class SmartHomeView(http.HomeAssistantView):
return b'' if response is None else self.json(response)
@asyncio.coroutine
def async_handle_message(hass, config, message):
async def async_handle_message(hass, config, request, context=None):
"""Handle incoming API messages."""
assert message[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3'
assert request[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3'
if context is None:
context = ha.Context()
# Read head data
message = message[API_DIRECTIVE]
namespace = message[API_HEADER]['namespace']
name = message[API_HEADER]['name']
request = request[API_DIRECTIVE]
namespace = request[API_HEADER]['namespace']
name = request[API_HEADER]['name']
# Do we support this API request?
funct_ref = HANDLERS.get((namespace, name))
if not funct_ref:
if funct_ref:
response = await funct_ref(hass, config, request, context)
else:
_LOGGER.warning(
"Unsupported API request %s/%s", namespace, name)
return api_error(message)
response = api_error(request)
return (yield from funct_ref(hass, config, message))
request_info = {
'namespace': namespace,
'name': name,
}
if API_ENDPOINT in request and 'endpointId' in request[API_ENDPOINT]:
request_info['entity_id'] = \
request[API_ENDPOINT]['endpointId'].replace('#', '.')
response_header = response[API_EVENT][API_HEADER]
hass.bus.async_fire(EVENT_ALEXA_SMART_HOME, {
'request': request_info,
'response': {
'namespace': response_header['namespace'],
'name': response_header['name'],
}
}, context=context)
return response
def api_message(request,
@@ -784,8 +818,7 @@ def api_error(request,
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
@asyncio.coroutine
def async_api_discovery(hass, config, request):
async def async_api_discovery(hass, config, request, context):
"""Create a API formatted discovery response.
Async friendly.
@@ -800,7 +833,7 @@ def async_api_discovery(hass, config, request):
if entity.domain not in ENTITY_ADAPTERS:
continue
alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity)
alexa_entity = ENTITY_ADAPTERS[entity.domain](hass, config, entity)
endpoint = {
'displayCategories': alexa_entity.display_categories(),
@@ -827,8 +860,7 @@ def async_api_discovery(hass, config, request):
def extract_entity(funct):
"""Decorate for extract entity object from request."""
@asyncio.coroutine
def async_api_entity_wrapper(hass, config, request):
async def async_api_entity_wrapper(hass, config, request, context):
"""Process a turn on request."""
entity_id = request[API_ENDPOINT]['endpointId'].replace('#', '.')
@@ -839,15 +871,14 @@ def extract_entity(funct):
request[API_HEADER]['name'], entity_id)
return api_error(request, error_type='NO_SUCH_ENDPOINT')
return (yield from funct(hass, config, request, entity))
return await funct(hass, config, request, context, entity)
return async_api_entity_wrapper
@HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
@extract_entity
@asyncio.coroutine
def async_api_turn_on(hass, config, request, entity):
async def async_api_turn_on(hass, config, request, context, entity):
"""Process a turn on request."""
domain = entity.domain
if entity.domain == group.DOMAIN:
@@ -857,17 +888,16 @@ def async_api_turn_on(hass, config, request, entity):
if entity.domain == cover.DOMAIN:
service = cover.SERVICE_OPEN_COVER
yield from hass.services.async_call(domain, service, {
await hass.services.async_call(domain, service, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.PowerController', 'TurnOff'))
@extract_entity
@asyncio.coroutine
def async_api_turn_off(hass, config, request, entity):
async def async_api_turn_off(hass, config, request, context, entity):
"""Process a turn off request."""
domain = entity.domain
if entity.domain == group.DOMAIN:
@@ -877,32 +907,30 @@ def async_api_turn_off(hass, config, request, entity):
if entity.domain == cover.DOMAIN:
service = cover.SERVICE_CLOSE_COVER
yield from hass.services.async_call(domain, service, {
await hass.services.async_call(domain, service, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness'))
@extract_entity
@asyncio.coroutine
def async_api_set_brightness(hass, config, request, entity):
async def async_api_set_brightness(hass, config, request, context, entity):
"""Process a set brightness request."""
brightness = int(request[API_PAYLOAD]['brightness'])
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_BRIGHTNESS_PCT: brightness,
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness'))
@extract_entity
@asyncio.coroutine
def async_api_adjust_brightness(hass, config, request, entity):
async def async_api_adjust_brightness(hass, config, request, context, entity):
"""Process an adjust brightness request."""
brightness_delta = int(request[API_PAYLOAD]['brightnessDelta'])
@@ -915,18 +943,17 @@ def async_api_adjust_brightness(hass, config, request, entity):
# set brightness
brightness = max(0, brightness_delta + current)
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_BRIGHTNESS_PCT: brightness,
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.ColorController', 'SetColor'))
@extract_entity
@asyncio.coroutine
def async_api_set_color(hass, config, request, entity):
async def async_api_set_color(hass, config, request, context, entity):
"""Process a set color request."""
rgb = color_util.color_hsb_to_RGB(
float(request[API_PAYLOAD]['color']['hue']),
@@ -934,25 +961,25 @@ def async_api_set_color(hass, config, request, entity):
float(request[API_PAYLOAD]['color']['brightness'])
)
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_RGB_COLOR: rgb,
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature'))
@extract_entity
@asyncio.coroutine
def async_api_set_color_temperature(hass, config, request, entity):
async def async_api_set_color_temperature(hass, config, request, context,
entity):
"""Process a set color temperature request."""
kelvin = int(request[API_PAYLOAD]['colorTemperatureInKelvin'])
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_KELVIN: kelvin,
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@@ -960,17 +987,17 @@ def async_api_set_color_temperature(hass, config, request, entity):
@HANDLERS.register(
('Alexa.ColorTemperatureController', 'DecreaseColorTemperature'))
@extract_entity
@asyncio.coroutine
def async_api_decrease_color_temp(hass, config, request, entity):
async def async_api_decrease_color_temp(hass, config, request, context,
entity):
"""Process a decrease color temperature request."""
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS))
value = min(max_mireds, current + 50)
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_COLOR_TEMP: value,
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@@ -978,31 +1005,30 @@ def async_api_decrease_color_temp(hass, config, request, entity):
@HANDLERS.register(
('Alexa.ColorTemperatureController', 'IncreaseColorTemperature'))
@extract_entity
@asyncio.coroutine
def async_api_increase_color_temp(hass, config, request, entity):
async def async_api_increase_color_temp(hass, config, request, context,
entity):
"""Process an increase color temperature request."""
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))
value = max(min_mireds, current - 50)
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_COLOR_TEMP: value,
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.SceneController', 'Activate'))
@extract_entity
@asyncio.coroutine
def async_api_activate(hass, config, request, entity):
async def async_api_activate(hass, config, request, context, entity):
"""Process an activate request."""
domain = entity.domain
yield from hass.services.async_call(domain, SERVICE_TURN_ON, {
await hass.services.async_call(domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False)
}, blocking=False, context=context)
payload = {
'cause': {'type': _Cause.VOICE_INTERACTION},
@@ -1019,14 +1045,13 @@ def async_api_activate(hass, config, request, entity):
@HANDLERS.register(('Alexa.SceneController', 'Deactivate'))
@extract_entity
@asyncio.coroutine
def async_api_deactivate(hass, config, request, entity):
async def async_api_deactivate(hass, config, request, context, entity):
"""Process a deactivate request."""
domain = entity.domain
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, {
await hass.services.async_call(domain, SERVICE_TURN_OFF, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False)
}, blocking=False, context=context)
payload = {
'cause': {'type': _Cause.VOICE_INTERACTION},
@@ -1043,8 +1068,7 @@ def async_api_deactivate(hass, config, request, entity):
@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage'))
@extract_entity
@asyncio.coroutine
def async_api_set_percentage(hass, config, request, entity):
async def async_api_set_percentage(hass, config, request, context, entity):
"""Process a set percentage request."""
percentage = int(request[API_PAYLOAD]['percentage'])
service = None
@@ -1066,16 +1090,15 @@ 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=False)
await hass.services.async_call(
entity.domain, service, data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage'))
@extract_entity
@asyncio.coroutine
def async_api_adjust_percentage(hass, config, request, entity):
async def async_api_adjust_percentage(hass, config, request, context, entity):
"""Process an adjust percentage request."""
percentage_delta = int(request[API_PAYLOAD]['percentageDelta'])
service = None
@@ -1114,20 +1137,19 @@ 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=False)
await hass.services.async_call(
entity.domain, service, data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.LockController', 'Lock'))
@extract_entity
@asyncio.coroutine
def async_api_lock(hass, config, request, entity):
async def async_api_lock(hass, config, request, context, entity):
"""Process a lock request."""
yield from hass.services.async_call(entity.domain, SERVICE_LOCK, {
await hass.services.async_call(entity.domain, SERVICE_LOCK, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False)
}, blocking=False, context=context)
# Alexa expects a lockState in the response, we don't know the actual
# lockState at this point but assume it is locked. It is reported
@@ -1144,20 +1166,18 @@ def async_api_lock(hass, config, request, entity):
# Not supported by Alexa yet
@HANDLERS.register(('Alexa.LockController', 'Unlock'))
@extract_entity
@asyncio.coroutine
def async_api_unlock(hass, config, request, entity):
async def async_api_unlock(hass, config, request, context, entity):
"""Process an unlock request."""
yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
await hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.Speaker', 'SetVolume'))
@extract_entity
@asyncio.coroutine
def async_api_set_volume(hass, config, request, entity):
async def async_api_set_volume(hass, config, request, context, entity):
"""Process a set volume request."""
volume = round(float(request[API_PAYLOAD]['volume'] / 100), 2)
@@ -1166,17 +1186,16 @@ def async_api_set_volume(hass, config, request, entity):
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, SERVICE_VOLUME_SET,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.InputController', 'SelectInput'))
@extract_entity
@asyncio.coroutine
def async_api_select_input(hass, config, request, entity):
async def async_api_select_input(hass, config, request, context, entity):
"""Process a set input request."""
media_input = request[API_PAYLOAD]['input']
@@ -1200,17 +1219,16 @@ def async_api_select_input(hass, config, request, entity):
media_player.ATTR_INPUT_SOURCE: media_input,
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, media_player.SERVICE_SELECT_SOURCE,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume'))
@extract_entity
@asyncio.coroutine
def async_api_adjust_volume(hass, config, request, entity):
async def async_api_adjust_volume(hass, config, request, context, entity):
"""Process an adjust volume request."""
volume_delta = int(request[API_PAYLOAD]['volume'])
@@ -1229,17 +1247,16 @@ def async_api_adjust_volume(hass, config, request, entity):
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, media_player.SERVICE_VOLUME_SET,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.StepSpeaker', 'AdjustVolume'))
@extract_entity
@asyncio.coroutine
def async_api_adjust_volume_step(hass, config, request, entity):
async def async_api_adjust_volume_step(hass, config, request, context, entity):
"""Process an adjust volume step request."""
# media_player volume up/down service does not support specifying steps
# each component handles it differently e.g. via config.
@@ -1252,13 +1269,13 @@ def async_api_adjust_volume_step(hass, config, request, entity):
}
if volume_step > 0:
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, media_player.SERVICE_VOLUME_UP,
data, blocking=False)
data, blocking=False, context=context)
elif volume_step < 0:
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, media_player.SERVICE_VOLUME_DOWN,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@@ -1266,8 +1283,7 @@ def async_api_adjust_volume_step(hass, config, request, entity):
@HANDLERS.register(('Alexa.StepSpeaker', 'SetMute'))
@HANDLERS.register(('Alexa.Speaker', 'SetMute'))
@extract_entity
@asyncio.coroutine
def async_api_set_mute(hass, config, request, entity):
async def async_api_set_mute(hass, config, request, context, entity):
"""Process a set mute request."""
mute = bool(request[API_PAYLOAD]['mute'])
@@ -1276,98 +1292,94 @@ def async_api_set_mute(hass, config, request, entity):
media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, media_player.SERVICE_VOLUME_MUTE,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Play'))
@extract_entity
@asyncio.coroutine
def async_api_play(hass, config, request, entity):
async def async_api_play(hass, config, request, context, entity):
"""Process a play request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_PLAY,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Pause'))
@extract_entity
@asyncio.coroutine
def async_api_pause(hass, config, request, entity):
async def async_api_pause(hass, config, request, context, entity):
"""Process a pause request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_PAUSE,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Stop'))
@extract_entity
@asyncio.coroutine
def async_api_stop(hass, config, request, entity):
async def async_api_stop(hass, config, request, context, entity):
"""Process a stop request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_STOP,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Next'))
@extract_entity
@asyncio.coroutine
def async_api_next(hass, config, request, entity):
async def async_api_next(hass, config, request, context, entity):
"""Process a next request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_NEXT_TRACK,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Previous'))
@extract_entity
@asyncio.coroutine
def async_api_previous(hass, config, request, entity):
async def async_api_previous(hass, config, request, context, entity):
"""Process a previous request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_PREVIOUS_TRACK,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
def api_error_temp_range(request, temp, min_temp, max_temp, unit):
def api_error_temp_range(hass, request, temp, min_temp, max_temp):
"""Create temperature value out of range API error response.
Async friendly.
"""
unit = hass.config.units.temperature_unit
temp_range = {
'minimumValue': {
'value': min_temp,
@@ -1388,8 +1400,9 @@ def api_error_temp_range(request, temp, min_temp, max_temp, unit):
)
def temperature_from_object(temp_obj, to_unit, interval=False):
def temperature_from_object(hass, temp_obj, interval=False):
"""Get temperature from Temperature object in requested unit."""
to_unit = hass.config.units.temperature_unit
from_unit = TEMP_CELSIUS
temp = float(temp_obj['value'])
@@ -1405,9 +1418,8 @@ def temperature_from_object(temp_obj, to_unit, interval=False):
@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature'))
@extract_entity
async def async_api_set_target_temp(hass, config, request, entity):
async def async_api_set_target_temp(hass, config, request, context, entity):
"""Process a set target temperature request."""
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
@@ -1417,48 +1429,45 @@ async def async_api_set_target_temp(hass, config, request, entity):
payload = request[API_PAYLOAD]
if 'targetSetpoint' in payload:
temp = temperature_from_object(
payload['targetSetpoint'], unit)
temp = temperature_from_object(hass, payload['targetSetpoint'])
if temp < min_temp or temp > max_temp:
return api_error_temp_range(
request, temp, min_temp, max_temp, unit)
hass, request, temp, min_temp, max_temp)
data[ATTR_TEMPERATURE] = temp
if 'lowerSetpoint' in payload:
temp_low = temperature_from_object(
payload['lowerSetpoint'], unit)
temp_low = temperature_from_object(hass, payload['lowerSetpoint'])
if temp_low < min_temp or temp_low > max_temp:
return api_error_temp_range(
request, temp_low, min_temp, max_temp, unit)
hass, request, temp_low, min_temp, max_temp)
data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
if 'upperSetpoint' in payload:
temp_high = temperature_from_object(
payload['upperSetpoint'], unit)
temp_high = temperature_from_object(hass, payload['upperSetpoint'])
if temp_high < min_temp or temp_high > max_temp:
return api_error_temp_range(
request, temp_high, min_temp, max_temp, unit)
hass, request, temp_high, min_temp, max_temp)
data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False,
context=context)
return api_message(request)
@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature'))
@extract_entity
async def async_api_adjust_target_temp(hass, config, request, entity):
async def async_api_adjust_target_temp(hass, config, request, context, entity):
"""Process an adjust target temperature request."""
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
temp_delta = temperature_from_object(
request[API_PAYLOAD]['targetSetpointDelta'], unit, interval=True)
hass, request[API_PAYLOAD]['targetSetpointDelta'], interval=True)
target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta
if target_temp < min_temp or target_temp > max_temp:
return api_error_temp_range(
request, target_temp, min_temp, max_temp, unit)
hass, request, target_temp, min_temp, max_temp)
data = {
ATTR_ENTITY_ID: entity.entity_id,
@@ -1466,14 +1475,16 @@ async def async_api_adjust_target_temp(hass, config, request, entity):
}
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False,
context=context)
return api_message(request)
@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode'))
@extract_entity
async def async_api_set_thermostat_mode(hass, config, request, entity):
async def async_api_set_thermostat_mode(hass, config, request, context,
entity):
"""Process a set thermostat mode request."""
mode = request[API_PAYLOAD]['thermostatMode']
mode = mode if isinstance(mode, str) else mode['value']
@@ -1499,17 +1510,16 @@ async def async_api_set_thermostat_mode(hass, config, request, entity):
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_OPERATION_MODE, data,
blocking=False)
blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa', 'ReportState'))
@extract_entity
@asyncio.coroutine
def async_api_reportstate(hass, config, request, entity):
async def async_api_reportstate(hass, config, request, context, entity):
"""Process a ReportState request."""
alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity)
alexa_entity = ENTITY_ADAPTERS[entity.domain](hass, config, entity)
properties = []
for interface in alexa_entity.interfaces():
properties.extend(interface.serialize_properties())

View File

@@ -24,7 +24,7 @@ from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers.state import AsyncTrackStates
import homeassistant.remote as rem
from homeassistant.helpers.json import JSONEncoder
_LOGGER = logging.getLogger(__name__)
@@ -102,7 +102,7 @@ class APIEventStream(HomeAssistantView):
if event.event_type == EVENT_HOMEASSISTANT_STOP:
data = stop_obj
else:
data = json.dumps(event, cls=rem.JSONEncoder)
data = json.dumps(event, cls=JSONEncoder)
await to_write.put(data)

View File

@@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
_CONFIGURING = {}
REQUIREMENTS = ['py-august==0.4.0']
REQUIREMENTS = ['py-august==0.6.0']
DEFAULT_TIMEOUT = 10
ACTIVITY_FETCH_LIMIT = 10

View File

@@ -0,0 +1,16 @@
{
"mfa_setup": {
"totp": {
"error": {
"invalid_code": "Codi no v\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa."
},
"step": {
"init": {
"description": "Per activar la verificaci\u00f3 en dos passos mitjan\u00e7ant contrasenyes d'un sol \u00fas basades en temps, escanegeu el codi QR amb la vostre aplicaci\u00f3 de verificaci\u00f3. Si no en teniu cap, us recomanem [Google Authenticator](https://support.google.com/accounts/answer/1066447) o b\u00e9 [Authy](https://authy.com/). \n\n {qr_code} \n \nDespr\u00e9s d'escanejar el codi QR, introdu\u00efu el codi de sis d\u00edgits proporcionat per l'aplicaci\u00f3. Si teniu problemes per escanejar el codi QR, feu una configuraci\u00f3 manual amb el codi **`{code}`**.",
"title": "Configureu la verificaci\u00f3 en dos passos utilitzant TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@@ -0,0 +1,16 @@
{
"mfa_setup": {
"totp": {
"error": {
"invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate."
},
"step": {
"init": {
"description": "To activate two factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**.",
"title": "Set up two-factor authentication using TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@@ -0,0 +1,16 @@
{
"mfa_setup": {
"totp": {
"error": {
"invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc \uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant \uc758 \uc2dc\uacc4\uac00 \uc815\ud655\ud55c\uc9c0 \ud655\uc778\ud574\ubcf4\uc138\uc694."
},
"step": {
"init": {
"description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574 \uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \ub098 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.",
"title": "TOTP \ub97c \uc0ac\uc6a9\ud558\uc5ec 2 \ub2e8\uacc4 \uc778\uc99d \uad6c\uc131"
}
},
"title": "TOTP (\uc2dc\uac04 \uae30\ubc18 OTP)"
}
}
}

View File

@@ -0,0 +1,16 @@
{
"mfa_setup": {
"totp": {
"error": {
"invalid_code": "Ong\u00ebltege Login, prob\u00e9iert w.e.g. nach emol. Falls d\u00ebse Feeler Message \u00ebmmer er\u00ebm optr\u00ebtt dann iwwerpr\u00e9ift op d'Z\u00e4it vum Home Assistant System richteg ass."
},
"step": {
"init": {
"description": "Fir d'Zwee-Faktor-Authentifikatioun m\u00ebttels engem Z\u00e4it bas\u00e9ierten eemolege Passwuert z'aktiv\u00e9ieren, scannt de QR Code mat enger Authentifikatioun's App.\nFalls dir keng hutt, recommand\u00e9iere mir entweder [Google Authenticator](https://support.google.com/accounts/answer/1066447) oder [Authy](https://authy.com/).\n\n{qr_code}\n\nNodeems de Code gescannt ass, gitt de sechs stellege Code vun der App a fir d'Konfiguratioun z'iwwerpr\u00e9iwen. Am Fall vu Problemer fir de QR Code ze scannen, gitt de folgende Code **`{code}`** a fir ee manuelle Setup.",
"title": "Zwee Faktor Authentifikatioun mat TOTP konfigur\u00e9ieren"
}
},
"title": "TOTP"
}
}
}

View File

@@ -0,0 +1,16 @@
{
"mfa_setup": {
"totp": {
"error": {
"invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0415\u0441\u043b\u0438 \u0432\u044b \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0435 \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0447\u0430\u0441\u044b \u0432 \u0432\u0430\u0448\u0435\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Home Assistant \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0442 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f."
},
"step": {
"init": {
"description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043e\u0442\u0441\u043a\u0430\u043d\u0438\u0440\u0443\u0439\u0442\u0435 QR-\u043a\u043e\u0434 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u043e\u0434\u043b\u0438\u043d\u043d\u043e\u0441\u0442\u0438. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0433\u043e \u043d\u0435\u0442, \u043c\u044b \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043b\u0438\u0431\u043e [Google Authenticator] (https://support.google.com/accounts/answer/1066447), \u043b\u0438\u0431\u043e [Authy] (https://authy.com/). \n\n {qr_code} \n \n \u041f\u043e\u0441\u043b\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f QR-\u043a\u043e\u0434\u0430 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0448\u0435\u0441\u0442\u0438\u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u0437 \u0432\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c QR-\u043a\u043e\u0434\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0440\u0443\u0447\u043d\u0443\u044e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0441 \u043a\u043e\u0434\u043e\u043c ** ` {code} ` **.",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@@ -0,0 +1,16 @@
{
"mfa_setup": {
"totp": {
"error": {
"invalid_code": "\u53e3\u4ee4\u65e0\u6548\uff0c\u8bf7\u91cd\u65b0\u8f93\u5165\u3002\u5982\u679c\u9519\u8bef\u53cd\u590d\u51fa\u73b0\uff0c\u8bf7\u786e\u4fdd Home Assistant \u7cfb\u7edf\u7684\u65f6\u95f4\u51c6\u786e\u65e0\u8bef\u3002"
},
"step": {
"init": {
"description": "\u8981\u6fc0\u6d3b\u57fa\u4e8e\u65f6\u95f4\u52a8\u6001\u53e3\u4ee4\u7684\u53cc\u91cd\u8ba4\u8bc1\uff0c\u8bf7\u7528\u8eab\u4efd\u9a8c\u8bc1\u5e94\u7528\u626b\u63cf\u4ee5\u4e0b\u4e8c\u7ef4\u7801\u3002\u5982\u679c\u60a8\u8fd8\u6ca1\u6709\u8eab\u4efd\u9a8c\u8bc1\u5e94\u7528\uff0c\u63a8\u8350\u4f7f\u7528 [Google \u8eab\u4efd\u9a8c\u8bc1\u5668](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u626b\u63cf\u4e8c\u7ef4\u7801\u4ee5\u540e\uff0c\u8f93\u5165\u5e94\u7528\u4e0a\u7684\u516d\u4f4d\u6570\u5b57\u53e3\u4ee4\u6765\u9a8c\u8bc1\u914d\u7f6e\u3002\u5982\u679c\u5728\u626b\u63cf\u4e8c\u7ef4\u7801\u65f6\u9047\u5230\u95ee\u9898\uff0c\u8bf7\u4f7f\u7528\u4ee3\u7801 **`{code}`** \u624b\u52a8\u914d\u7f6e\u3002",
"title": "\u7528\u65f6\u95f4\u52a8\u6001\u53e3\u4ee4\u8bbe\u7f6e\u53cc\u91cd\u8ba4\u8bc1"
}
},
"title": "\u65f6\u95f4\u52a8\u6001\u53e3\u4ee4"
}
}
}

View File

@@ -0,0 +1,16 @@
{
"mfa_setup": {
"totp": {
"error": {
"invalid_code": "\u9a57\u8b49\u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002\u5047\u5982\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u5148\u78ba\u5b9a\u60a8\u7684 Home Assistant \u7cfb\u7d71\u4e0a\u7684\u6642\u9593\u8a2d\u5b9a\u6b63\u78ba\u5f8c\uff0c\u518d\u8a66\u4e00\u6b21\u3002"
},
"step": {
"init": {
"description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u5169\u6b65\u9a5f\u9a57\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u6383\u63cf\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u6383\u63cf\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002",
"title": "\u4f7f\u7528 TOTP \u8a2d\u5b9a\u5169\u6b65\u9a5f\u9a57\u8b49"
}
},
"title": "TOTP"
}
}
}

View File

@@ -44,21 +44,36 @@ a limited expiration.
"expires_in": 1800,
"token_type": "Bearer"
}
## Revoking a refresh token
It is also possible to revoke a refresh token and all access tokens that have
ever been granted by that refresh token. Response code will ALWAYS be 200.
{
"token": "IJKLMNOPQRST",
"action": "revoke"
}
"""
import logging
import uuid
from datetime import timedelta
from aiohttp import web
import voluptuous as vol
from homeassistant.auth.models import User, Credentials
from homeassistant.components import websocket_api
from homeassistant.components.http.ban import log_invalid_auth
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.core import callback
from homeassistant.core import callback, HomeAssistant
from homeassistant.util import dt as dt_util
from . import indieauth
from . import login_flow
from . import mfa_setup_flow
DOMAIN = 'auth'
DEPENDENCIES = ['http']
@@ -68,37 +83,41 @@ SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_CURRENT_USER,
})
RESULT_TYPE_CREDENTIALS = 'credentials'
RESULT_TYPE_USER = 'user'
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config):
"""Component to allow users to login."""
store_credentials, retrieve_credentials = _create_cred_store()
store_result, retrieve_result = _create_auth_code_store()
hass.http.register_view(GrantTokenView(retrieve_credentials))
hass.http.register_view(LinkUserView(retrieve_credentials))
hass.http.register_view(TokenView(retrieve_result))
hass.http.register_view(LinkUserView(retrieve_result))
hass.components.websocket_api.async_register_command(
WS_TYPE_CURRENT_USER, websocket_current_user,
SCHEMA_WS_CURRENT_USER
)
await login_flow.async_setup(hass, store_credentials)
await login_flow.async_setup(hass, store_result)
await mfa_setup_flow.async_setup(hass)
return True
class GrantTokenView(HomeAssistantView):
"""View to grant tokens."""
class TokenView(HomeAssistantView):
"""View to issue or revoke tokens."""
url = '/auth/token'
name = 'api:auth:token'
requires_auth = False
cors_allowed = True
def __init__(self, retrieve_credentials):
"""Initialize the grant token view."""
self._retrieve_credentials = retrieve_credentials
def __init__(self, retrieve_user):
"""Initialize the token view."""
self._retrieve_user = retrieve_user
@log_invalid_auth
async def post(self, request):
@@ -108,6 +127,13 @@ class GrantTokenView(HomeAssistantView):
grant_type = data.get('grant_type')
# IndieAuth 6.3.5
# The revocation endpoint is the same as the token endpoint.
# The revocation request includes an additional parameter,
# action=revoke.
if data.get('action') == 'revoke':
return await self._async_handle_revoke_token(hass, data)
if grant_type == 'authorization_code':
return await self._async_handle_auth_code(hass, data)
@@ -118,6 +144,25 @@ class GrantTokenView(HomeAssistantView):
'error': 'unsupported_grant_type',
}, status_code=400)
async def _async_handle_revoke_token(self, hass, data):
"""Handle revoke token request."""
# OAuth 2.0 Token Revocation [RFC7009]
# 2.2 The authorization server responds with HTTP status code 200
# if the token has been revoked successfully or if the client
# submitted an invalid token.
token = data.get('token')
if token is None:
return web.Response(status=200)
refresh_token = await hass.auth.async_get_refresh_token_by_token(token)
if refresh_token is None:
return web.Response(status=200)
await hass.auth.async_remove_refresh_token(refresh_token)
return web.Response(status=200)
async def _async_handle_auth_code(self, hass, data):
"""Handle authorization code request."""
client_id = data.get('client_id')
@@ -132,17 +177,19 @@ class GrantTokenView(HomeAssistantView):
if code is None:
return self.json({
'error': 'invalid_request',
'error_description': 'Invalid code',
}, status_code=400)
credentials = self._retrieve_credentials(client_id, code)
user = self._retrieve_user(client_id, RESULT_TYPE_USER, code)
if credentials is None:
if user is None or not isinstance(user, User):
return self.json({
'error': 'invalid_request',
'error_description': 'Invalid code',
}, status_code=400)
user = await hass.auth.async_get_or_create_user(credentials)
# refresh user
user = await hass.auth.async_get_user(user.id)
if not user.is_active:
return self.json({
@@ -155,7 +202,7 @@ class GrantTokenView(HomeAssistantView):
access_token = hass.auth.async_create_access_token(refresh_token)
return self.json({
'access_token': access_token.token,
'access_token': access_token,
'token_type': 'Bearer',
'refresh_token': refresh_token.token,
'expires_in':
@@ -178,7 +225,7 @@ class GrantTokenView(HomeAssistantView):
'error': 'invalid_request',
}, status_code=400)
refresh_token = await hass.auth.async_get_refresh_token(token)
refresh_token = await hass.auth.async_get_refresh_token_by_token(token)
if refresh_token is None:
return self.json({
@@ -193,7 +240,7 @@ class GrantTokenView(HomeAssistantView):
access_token = hass.auth.async_create_access_token(refresh_token)
return self.json({
'access_token': access_token.token,
'access_token': access_token,
'token_type': 'Bearer',
'expires_in':
int(refresh_token.access_token_expiration.total_seconds()),
@@ -220,7 +267,7 @@ class LinkUserView(HomeAssistantView):
user = request['hass_user']
credentials = self._retrieve_credentials(
data['client_id'], data['code'])
data['client_id'], RESULT_TYPE_CREDENTIALS, data['code'])
if credentials is None:
return self.json_message('Invalid code', status_code=400)
@@ -230,54 +277,69 @@ class LinkUserView(HomeAssistantView):
@callback
def _create_cred_store():
"""Create a credential store."""
temp_credentials = {}
def _create_auth_code_store():
"""Create an in memory store."""
temp_results = {}
@callback
def store_credentials(client_id, credentials):
"""Store credentials and return a code to retrieve it."""
def store_result(client_id, result):
"""Store flow result and return a code to retrieve it."""
if isinstance(result, User):
result_type = RESULT_TYPE_USER
elif isinstance(result, Credentials):
result_type = RESULT_TYPE_CREDENTIALS
else:
raise ValueError('result has to be either User or Credentials')
code = uuid.uuid4().hex
temp_credentials[(client_id, code)] = (dt_util.utcnow(), credentials)
temp_results[(client_id, result_type, code)] = \
(dt_util.utcnow(), result_type, result)
return code
@callback
def retrieve_credentials(client_id, code):
"""Retrieve credentials."""
key = (client_id, code)
def retrieve_result(client_id, result_type, code):
"""Retrieve flow result."""
key = (client_id, result_type, code)
if key not in temp_credentials:
if key not in temp_results:
return None
created, credentials = temp_credentials.pop(key)
created, _, result = temp_results.pop(key)
# OAuth 4.2.1
# The authorization code MUST expire shortly after it is issued to
# mitigate the risk of leaks. A maximum authorization code lifetime of
# 10 minutes is RECOMMENDED.
if dt_util.utcnow() - created < timedelta(minutes=10):
return credentials
return result
return None
return store_credentials, retrieve_credentials
return store_result, retrieve_result
@websocket_api.ws_require_user()
@callback
def websocket_current_user(hass, connection, msg):
def websocket_current_user(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
"""Return the current user."""
user = connection.request.get('hass_user')
async def async_get_current_user(user):
"""Get current user."""
enabled_modules = await hass.auth.async_get_enabled_mfa(user)
if user is None:
connection.to_write.put_nowait(websocket_api.error_message(
msg['id'], 'no_user', 'Not authenticated as a user'))
return
connection.send_message_outside(
websocket_api.result_message(msg['id'], {
'id': user.id,
'name': user.name,
'is_owner': user.is_owner,
'credentials': [{'auth_provider_type': c.auth_provider_type,
'auth_provider_id': c.auth_provider_id}
for c in user.credentials],
'mfa_modules': [{
'id': module.id,
'name': module.name,
'enabled': module.id in enabled_modules,
} for module in hass.auth.auth_mfa_modules],
}))
connection.to_write.put_nowait(websocket_api.result_message(msg['id'], {
'id': user.id,
'name': user.name,
'is_owner': user.is_owner,
'credentials': [{'auth_provider_type': c.auth_provider_type,
'auth_provider_id': c.auth_provider_id}
for c in user.credentials]
}))
hass.async_create_task(async_get_current_user(connection.user))

View File

@@ -1,6 +1,11 @@
"""Helpers to resolve client ID/secret."""
import asyncio
from html.parser import HTMLParser
from ipaddress import ip_address, ip_network
from urllib.parse import urlparse
from urllib.parse import urlparse, urljoin
import aiohttp
from aiohttp.client_exceptions import ClientError
# IP addresses of loopback interfaces
ALLOWED_IPS = (
@@ -16,7 +21,7 @@ ALLOWED_NETWORKS = (
)
def verify_redirect_uri(client_id, redirect_uri):
async def verify_redirect_uri(hass, client_id, redirect_uri):
"""Verify that the client and redirect uri match."""
try:
client_id_parts = _parse_client_id(client_id)
@@ -25,16 +30,74 @@ def verify_redirect_uri(client_id, redirect_uri):
redirect_parts = _parse_url(redirect_uri)
# IndieAuth 4.2.2 allows for redirect_uri to be on different domain
# but needs to be specified in link tag when fetching `client_id`.
# This is not implemented.
# Verify redirect url and client url have same scheme and domain.
return (
is_valid = (
client_id_parts.scheme == redirect_parts.scheme and
client_id_parts.netloc == redirect_parts.netloc
)
if is_valid:
return True
# IndieAuth 4.2.2 allows for redirect_uri to be on different domain
# but needs to be specified in link tag when fetching `client_id`.
redirect_uris = await fetch_redirect_uris(hass, client_id)
return redirect_uri in redirect_uris
class LinkTagParser(HTMLParser):
"""Parser to find link tags."""
def __init__(self, rel):
"""Initialize a link tag parser."""
super().__init__()
self.rel = rel
self.found = []
def handle_starttag(self, tag, attrs):
"""Handle finding a start tag."""
if tag != 'link':
return
attrs = dict(attrs)
if attrs.get('rel') == self.rel:
self.found.append(attrs.get('href'))
async def fetch_redirect_uris(hass, url):
"""Find link tag with redirect_uri values.
IndieAuth 4.2.2
The client SHOULD publish one or more <link> tags or Link HTTP headers with
a rel attribute of redirect_uri at the client_id URL.
We limit to the first 10kB of the page.
We do not implement extracting redirect uris from headers.
"""
parser = LinkTagParser('redirect_uri')
chunks = 0
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=5) as resp:
async for data in resp.content.iter_chunked(1024):
parser.feed(data.decode())
chunks += 1
if chunks == 10:
break
except (asyncio.TimeoutError, ClientError):
pass
# Authorization endpoints verifying that a redirect_uri is allowed for use
# by a client MUST look for an exact match of the given redirect_uri in the
# request against the list of redirect_uris discovered after resolving any
# relative URLs.
return [urljoin(url, found) for found in parser.found]
def verify_client_id(client_id):
"""Verify that the client id is valid."""

View File

@@ -22,10 +22,14 @@ Pass in parameter 'client_id' and 'redirect_url' validate by indieauth.
Pass in parameter 'handler' to specify the auth provider to use. Auth providers
are identified by type and id.
And optional parameter 'type' has to set as 'link_user' if login flow used for
link credential to exist user. Default 'type' is 'authorize'.
{
"client_id": "https://hassbian.local:8123/",
"handler": ["local_provider", null],
"redirect_url": "https://hassbian.local:8123/"
"redirect_url": "https://hassbian.local:8123/",
"type': "authorize"
}
Return value will be a step in a data entry flow. See the docs for data entry
@@ -49,12 +53,14 @@ flow for details.
Progress the flow. Most flows will be 1 page, but could optionally add extra
login challenges, like TFA. Once the flow has finished, the returned step will
have type "create_entry" and "result" key will contain an authorization code.
The authorization code associated with an authorized user by default, it will
associate with an credential if "type" set to "link_user" in
"/auth/login_flow"
{
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
"handler": ["insecure_example", null],
"result": "411ee2f916e648d691e937ae9344681e",
"source": "user",
"title": "Example",
"type": "create_entry",
"version": 1
@@ -64,21 +70,20 @@ import aiohttp.web
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.components.http import KEY_REAL_IP
from homeassistant.components.http.ban import process_wrong_login, \
log_invalid_auth
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView, FlowManagerResourceView)
from . import indieauth
async def async_setup(hass, store_credentials):
async def async_setup(hass, store_result):
"""Component to allow users to login."""
hass.http.register_view(AuthProvidersView)
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow))
hass.http.register_view(
LoginFlowResourceView(hass.auth.login_flow, store_credentials))
LoginFlowResourceView(hass.auth.login_flow, store_result))
class AuthProvidersView(HomeAssistantView):
@@ -97,13 +102,41 @@ class AuthProvidersView(HomeAssistantView):
} for provider in request.app['hass'].auth.auth_providers])
class LoginFlowIndexView(FlowManagerIndexView):
def _prepare_result_json(result):
"""Convert result to JSON."""
if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
data = result.copy()
data.pop('result')
data.pop('data')
return data
if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
return result
import voluptuous_serialize
data = result.copy()
schema = data['data_schema']
if schema is None:
data['data_schema'] = []
else:
data['data_schema'] = voluptuous_serialize.convert(schema)
return data
class LoginFlowIndexView(HomeAssistantView):
"""View to create a config flow."""
url = '/auth/login_flow'
name = 'api:auth:login_flow'
requires_auth = False
def __init__(self, flow_mgr):
"""Initialize the flow manager index view."""
self._flow_mgr = flow_mgr
async def get(self, request):
"""Do not allow index of flows in progress."""
return aiohttp.web.Response(status=405)
@@ -112,31 +145,47 @@ class LoginFlowIndexView(FlowManagerIndexView):
vol.Required('client_id'): str,
vol.Required('handler'): vol.Any(str, list),
vol.Required('redirect_uri'): str,
vol.Optional('type', default='authorize'): str,
}))
@log_invalid_auth
async def post(self, request, data):
"""Create a new login flow."""
if not indieauth.verify_redirect_uri(data['client_id'],
data['redirect_uri']):
if not await indieauth.verify_redirect_uri(
request.app['hass'], data['client_id'], data['redirect_uri']):
return self.json_message('invalid client id or redirect uri', 400)
# pylint: disable=no-value-for-parameter
return await super().post(request)
if isinstance(data['handler'], list):
handler = tuple(data['handler'])
else:
handler = data['handler']
try:
result = await self._flow_mgr.async_init(
handler, context={
'ip_address': request[KEY_REAL_IP],
'credential_only': data.get('type') == 'link_user',
})
except data_entry_flow.UnknownHandler:
return self.json_message('Invalid handler specified', 404)
except data_entry_flow.UnknownStep:
return self.json_message('Handler does not support init', 400)
return self.json(_prepare_result_json(result))
class LoginFlowResourceView(FlowManagerResourceView):
class LoginFlowResourceView(HomeAssistantView):
"""View to interact with the flow manager."""
url = '/auth/login_flow/{flow_id}'
name = 'api:auth:login_flow:resource'
requires_auth = False
def __init__(self, flow_mgr, store_credentials):
def __init__(self, flow_mgr, store_result):
"""Initialize the login flow resource view."""
super().__init__(flow_mgr)
self._store_credentials = store_credentials
self._flow_mgr = flow_mgr
self._store_result = store_result
async def get(self, request, flow_id):
async def get(self, request):
"""Do not allow getting status of a flow in progress."""
return self.json_message('Invalid flow specified', 404)
@@ -152,6 +201,13 @@ class LoginFlowResourceView(FlowManagerResourceView):
return self.json_message('Invalid client id', 400)
try:
# do not allow change ip during login flow
for flow in self._flow_mgr.async_progress():
if (flow['flow_id'] == flow_id and
flow['context']['ip_address'] !=
request.get(KEY_REAL_IP)):
return self.json_message('IP address changed', 400)
result = await self._flow_mgr.async_configure(flow_id, data)
except data_entry_flow.UnknownFlow:
return self.json_message('Invalid flow specified', 404)
@@ -164,9 +220,18 @@ class LoginFlowResourceView(FlowManagerResourceView):
if result['errors'] is not None and \
result['errors'].get('base') == 'invalid_auth':
await process_wrong_login(request)
return self.json(self._prepare_result_json(result))
return self.json(_prepare_result_json(result))
result.pop('data')
result['result'] = self._store_credentials(client_id, result['result'])
result['result'] = self._store_result(client_id, result['result'])
return self.json(result)
async def delete(self, request, flow_id):
"""Cancel a flow in progress."""
try:
self._flow_mgr.async_abort(flow_id)
except data_entry_flow.UnknownFlow:
return self.json_message('Invalid flow specified', 404)
return self.json_message('Flow aborted')

View File

@@ -0,0 +1,134 @@
"""Helpers to setup multi-factor auth module."""
import logging
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.components import websocket_api
from homeassistant.core import callback, HomeAssistant
WS_TYPE_SETUP_MFA = 'auth/setup_mfa'
SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_SETUP_MFA,
vol.Exclusive('mfa_module_id', 'module_or_flow_id'): str,
vol.Exclusive('flow_id', 'module_or_flow_id'): str,
vol.Optional('user_input'): object,
})
WS_TYPE_DEPOSE_MFA = 'auth/depose_mfa'
SCHEMA_WS_DEPOSE_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_DEPOSE_MFA,
vol.Required('mfa_module_id'): str,
})
DATA_SETUP_FLOW_MGR = 'auth_mfa_setup_flow_manager'
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass):
"""Init mfa setup flow manager."""
async def _async_create_setup_flow(handler, context, data):
"""Create a setup flow. hanlder is a mfa module."""
mfa_module = hass.auth.get_auth_mfa_module(handler)
if mfa_module is None:
raise ValueError('Mfa module {} is not found'.format(handler))
user_id = data.pop('user_id')
return await mfa_module.async_setup_flow(user_id)
async def _async_finish_setup_flow(flow, flow_result):
_LOGGER.debug('flow_result: %s', flow_result)
return flow_result
hass.data[DATA_SETUP_FLOW_MGR] = data_entry_flow.FlowManager(
hass, _async_create_setup_flow, _async_finish_setup_flow)
hass.components.websocket_api.async_register_command(
WS_TYPE_SETUP_MFA, websocket_setup_mfa, SCHEMA_WS_SETUP_MFA)
hass.components.websocket_api.async_register_command(
WS_TYPE_DEPOSE_MFA, websocket_depose_mfa, SCHEMA_WS_DEPOSE_MFA)
@callback
@websocket_api.ws_require_user(allow_system_user=False)
def websocket_setup_mfa(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
"""Return a setup flow for mfa auth module."""
async def async_setup_flow(msg):
"""Return a setup flow for mfa auth module."""
flow_manager = hass.data[DATA_SETUP_FLOW_MGR]
flow_id = msg.get('flow_id')
if flow_id is not None:
result = await flow_manager.async_configure(
flow_id, msg.get('user_input'))
connection.send_message_outside(
websocket_api.result_message(
msg['id'], _prepare_result_json(result)))
return
mfa_module_id = msg.get('mfa_module_id')
mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id)
if mfa_module is None:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'no_module',
'MFA module {} is not found'.format(mfa_module_id)))
return
result = await flow_manager.async_init(
mfa_module_id, data={'user_id': connection.user.id})
connection.send_message_outside(
websocket_api.result_message(
msg['id'], _prepare_result_json(result)))
hass.async_create_task(async_setup_flow(msg))
@callback
@websocket_api.ws_require_user(allow_system_user=False)
def websocket_depose_mfa(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
"""Remove user from mfa module."""
async def async_depose(msg):
"""Remove user from mfa auth module."""
mfa_module_id = msg['mfa_module_id']
try:
await hass.auth.async_disable_user_mfa(
connection.user, msg['mfa_module_id'])
except ValueError as err:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'disable_failed',
'Cannot disable MFA Module {}: {}'.format(
mfa_module_id, err)))
return
connection.send_message_outside(
websocket_api.result_message(
msg['id'], 'done'))
hass.async_create_task(async_depose(msg))
def _prepare_result_json(result):
"""Convert result to JSON."""
if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
data = result.copy()
return data
if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
return result
import voluptuous_serialize
data = result.copy()
schema = data['data_schema']
if schema is None:
data['data_schema'] = []
else:
data['data_schema'] = voluptuous_serialize.convert(schema)
return data

View File

@@ -0,0 +1,16 @@
{
"mfa_setup":{
"totp": {
"title": "TOTP",
"step": {
"init": {
"title": "Set up two-factor authentication using TOTP",
"description": "To activate two factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**."
}
},
"error": {
"invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate."
}
}
}
}

View File

@@ -1,5 +1,5 @@
"""
Allow to setup simple automation rules via the config file.
Allow to set up simple automation rules via the config file.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/automation/

View File

@@ -58,7 +58,7 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, entry):
"""Setup a config entry."""
"""Set up a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)

View File

@@ -16,7 +16,7 @@ DEPENDENCIES = ['abode']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a sensor for an Abode device."""
import abodepy.helpers.constants as CONST
import abodepy.helpers.timeline as TIMELINE
@@ -44,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
data.devices.extend(devices)
add_devices(devices)
add_entities(devices)
class AbodeBinarySensor(AbodeDevice, BinarySensorDevice):

View File

@@ -27,7 +27,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Binary Sensor platform for ADS."""
ads_hub = hass.data.get(DATA_ADS)
@@ -36,7 +36,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
device_class = config.get(CONF_DEVICE_CLASS)
ads_sensor = AdsBinarySensor(ads_hub, name, ads_var, device_class)
add_devices([ads_sensor])
add_entities([ads_sensor])
class AdsBinarySensor(BinarySensorDevice):

View File

@@ -28,7 +28,7 @@ ATTR_RF_LOOP4 = 'rf_loop4'
ATTR_RF_LOOP1 = 'rf_loop1'
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the AlarmDecoder binary sensor devices."""
configured_zones = discovery_info[CONF_ZONES]
@@ -44,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
zone_num, zone_name, zone_type, zone_rfid, relay_addr, relay_chan)
devices.append(device)
add_devices(devices)
add_entities(devices)
return True

View File

@@ -14,7 +14,8 @@ DEPENDENCIES = ['android_ip_webcam']
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the IP Webcam binary sensors."""
if discovery_info is None:
return
@@ -23,7 +24,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
name = discovery_info[CONF_NAME]
ipcam = hass.data[DATA_IP_WEBCAM][host]
async_add_devices(
async_add_entities(
[IPWebcamBinarySensor(name, host, ipcam, 'motion_active')], True)

View File

@@ -20,9 +20,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up an APCUPSd Online Status binary sensor."""
add_devices([OnlineStatus(config, apcupsd.DATA)], True)
add_entities([OnlineStatus(config, apcupsd.DATA)], True)
class OnlineStatus(BinarySensorDevice):

View File

@@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the aREST binary sensor."""
resource = config.get(CONF_RESOURCE)
pin = config.get(CONF_PIN)
@@ -47,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
arest = ArestData(resource, pin)
add_devices([ArestBinarySensor(
add_entities([ArestBinarySensor(
arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
device_class, pin)], True)

View File

@@ -53,7 +53,7 @@ SENSOR_TYPES = {
}
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the August binary sensors."""
data = hass.data[DATA_AUGUST]
devices = []
@@ -62,7 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for sensor_type in SENSOR_TYPES:
devices.append(AugustBinarySensor(data, sensor_type, doorbell))
add_devices(devices, True)
add_entities(devices, True)
class AugustBinarySensor(BinarySensorDevice):

View File

@@ -39,7 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the aurora sensor."""
if None in (hass.config.latitude, hass.config.longitude):
_LOGGER.error("Lat. or long. not set in Home Assistant config")
@@ -57,7 +57,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"Connection to aurora forecast service failed: %s", error)
return False
add_devices([AuroraSensor(aurora_data, name)], True)
add_entities([AuroraSensor(aurora_data, name)], True)
class AuroraSensor(BinarySensorDevice):

View File

@@ -18,9 +18,9 @@ DEPENDENCIES = ['axis']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Axis binary devices."""
add_devices([AxisBinarySensor(hass, discovery_info)], True)
add_entities([AxisBinarySensor(hass, discovery_info)], True)
class AxisBinarySensor(AxisDeviceEvent, BinarySensorDevice):

View File

@@ -75,7 +75,8 @@ def update_probability(prior, prob_true, prob_false):
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Bayesian Binary sensor."""
name = config.get(CONF_NAME)
observations = config.get(CONF_OBSERVATIONS)
@@ -83,7 +84,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
probability_threshold = config.get(CONF_PROBABILITY_THRESHOLD)
device_class = config.get(CONF_DEVICE_CLASS)
async_add_devices([
async_add_entities([
BayesianBinarySensor(
name, prior, observations, probability_threshold, device_class)
], True)
@@ -122,7 +123,6 @@ class BayesianBinarySensor(BinarySensorDevice):
def async_added_to_hass(self):
"""Call when entity about to be added."""
@callback
# pylint: disable=invalid-name
def async_threshold_sensor_state_listener(entity, old_state,
new_state):
"""Handle sensor state changes."""

View File

@@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Beaglebone Black GPIO devices."""
pins = config.get(CONF_PINS)
@@ -49,7 +49,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for pin, params in pins.items():
binary_sensors.append(BBBGPIOBinarySensor(pin, params))
add_devices(binary_sensors)
add_entities(binary_sensors)
class BBBGPIOBinarySensor(BinarySensorDevice):

View File

@@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import BinarySensorDevice
DEPENDENCIES = ['blink']
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the blink binary sensors."""
if discovery_info is None:
return
@@ -20,7 +20,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for name in data.cameras:
devs.append(BlinkCameraMotionSensor(name, data))
devs.append(BlinkSystemSensor(data))
add_devices(devs, True)
add_entities(devs, True)
class BlinkCameraMotionSensor(BinarySensorDevice):

View File

@@ -28,7 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the available BloomSky weather binary sensors."""
bloomsky = hass.components.bloomsky
# Default needed in case of discovery
@@ -36,7 +36,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for device in bloomsky.BLOOMSKY.devices.values():
for variable in sensors:
add_devices(
add_entities(
[BloomSkySensor(bloomsky.BLOOMSKY, device, variable)], True)

View File

@@ -31,7 +31,7 @@ SENSOR_TYPES_ELEC = {
SENSOR_TYPES_ELEC.update(SENSOR_TYPES)
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the BMW sensors."""
accounts = hass.data[BMW_DOMAIN]
_LOGGER.debug('Found BMW accounts: %s',
@@ -51,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
device = BMWConnectedDriveSensor(account, vehicle, key,
value[0], value[1])
devices.append(device)
add_devices(devices, True)
add_entities(devices, True)
class BMWConnectedDriveSensor(BinarySensorDevice):
@@ -71,7 +71,10 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
@property
def should_poll(self) -> bool:
"""Data update is triggered from BMWConnectedDriveEntity."""
"""Return False.
Data update is triggered from BMWConnectedDriveEntity.
"""
return False
@property

View File

@@ -40,7 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Command line Binary Sensor."""
name = config.get(CONF_NAME)
command = config.get(CONF_COMMAND)
@@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
value_template.hass = hass
data = CommandSensorData(hass, command, command_timeout)
add_devices([CommandBinarySensor(
add_entities([CommandBinarySensor(
hass, data, name, device_class, payload_on, payload_off,
value_template)], True)

View File

@@ -42,7 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Concord232 binary sensor platform."""
from concord232 import client as concord232_client
@@ -79,7 +79,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
)
)
add_devices(sensors, True)
add_entities(sensors, True)
def get_opening_type(zone):

View File

@@ -7,21 +7,22 @@ https://home-assistant.io/components/binary_sensor.deconz/
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz.const import (
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB)
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN)
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['deconz']
async def async_setup_platform(hass, config, async_add_devices,
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Old way of setting up deCONZ binary sensors."""
pass
async def async_setup_entry(hass, config_entry, async_add_devices):
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ binary sensor."""
@callback
def async_add_sensor(sensors):
@@ -33,7 +34,7 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
if sensor.type in DECONZ_BINARY_SENSOR and \
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
entities.append(DeconzBinarySensor(sensor))
async_add_devices(entities, True)
async_add_entities(entities, True)
hass.data[DATA_DECONZ_UNSUB].append(
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
@@ -113,3 +114,19 @@ class DeconzBinarySensor(BinarySensorDevice):
if self._sensor.type in PRESENCE and self._sensor.dark is not None:
attr[ATTR_DARK] = self._sensor.dark
return attr
@property
def device_info(self):
"""Return a device description for device registry."""
if (self._sensor.uniqueid is None or
self._sensor.uniqueid.count(':') != 7):
return None
serial = self._sensor.uniqueid.split('-', 1)[0]
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},
'manufacturer': self._sensor.manufacturer,
'model': self._sensor.modelid,
'name': self._sensor.name,
'sw_version': self._sensor.swversion,
}

View File

@@ -7,9 +7,9 @@ https://home-assistant.io/components/demo/
from homeassistant.components.binary_sensor import BinarySensorDevice
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Demo binary sensor platform."""
add_devices([
add_entities([
DemoBinarySensor('Basement Floor Wet', False, 'moisture'),
DemoBinarySensor('Movement Backyard', True, 'motion'),
])

View File

@@ -28,7 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Digital Ocean droplet sensor."""
digital = hass.data.get(DATA_DIGITAL_OCEAN)
if not digital:
@@ -44,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False
dev.append(DigitalOceanBinarySensor(digital, droplet_id))
add_devices(dev, True)
add_entities(dev, True)
class DigitalOceanBinarySensor(BinarySensorDevice):

View File

@@ -12,7 +12,7 @@ DEPENDENCIES = ['ecobee']
ECOBEE_CONFIG_FILE = 'ecobee.conf'
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Ecobee sensors."""
if discovery_info is None:
return
@@ -26,7 +26,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev.append(EcobeeBinarySensor(sensor['name'], index))
add_devices(dev, True)
add_entities(dev, True)
class EcobeeBinarySensor(BinarySensorDevice):

View File

@@ -19,7 +19,8 @@ EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion',
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Initialize the platform."""
if (discovery_info is None or
discovery_info[ATTR_DISCOVER_DEVICES] is None):
@@ -27,7 +28,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
disc_info = discovery_info[ATTR_DISCOVER_DEVICES]
# multiple devices here!
async_add_devices(
async_add_entities(
(
EgardiaBinarySensor(
sensor_id=disc_info[sensor]['id'],
@@ -58,7 +59,7 @@ class EgardiaBinarySensor(BinarySensorDevice):
@property
def name(self):
"""The name of the device."""
"""Return the name of the device."""
return self._name
@property
@@ -74,5 +75,5 @@ class EgardiaBinarySensor(BinarySensorDevice):
@property
def device_class(self):
"""The device class."""
"""Return the device class."""
return self._device_class

View File

@@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['eight_sleep']
async def async_setup_platform(hass, config, async_add_devices,
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the eight sleep binary sensor."""
if discovery_info is None:
@@ -30,7 +30,7 @@ async def async_setup_platform(hass, config, async_add_devices,
for sensor in sensors:
all_sensors.append(EightHeatSensor(name, eight, sensor))
async_add_devices(all_sensors, True)
async_add_entities(all_sensors, True)
class EightHeatSensor(EightSleepHeatEntity, BinarySensorDevice):

View File

@@ -27,13 +27,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Binary Sensor platform for EnOcean."""
dev_id = config.get(CONF_ID)
devname = config.get(CONF_NAME)
device_class = config.get(CONF_DEVICE_CLASS)
add_devices([EnOceanBinarySensor(dev_id, devname, device_class)])
add_entities([EnOceanBinarySensor(dev_id, devname, device_class)])
class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):

View File

@@ -23,7 +23,8 @@ DEPENDENCIES = ['envisalink']
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Envisalink binary sensor devices."""
configured_zones = discovery_info['zones']
@@ -40,7 +41,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
)
devices.append(device)
async_add_devices(devices)
async_add_entities(devices)
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):

View File

@@ -47,7 +47,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the FFmpeg binary motion sensor."""
manager = hass.data[DATA_FFMPEG]
@@ -55,7 +56,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
return
entity = FFmpegMotion(hass, manager, config)
async_add_devices([entity])
async_add_entities([entity])
class FFmpegBinarySensor(FFmpegBase, BinarySensorDevice):

View File

@@ -44,7 +44,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the FFmpeg noise binary sensor."""
manager = hass.data[DATA_FFMPEG]
@@ -52,7 +53,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
return
entity = FFmpegNoise(hass, manager, config)
async_add_devices([entity])
async_add_entities([entity])
class FFmpegNoise(FFmpegBinarySensor):

View File

@@ -23,7 +23,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the GC100 devices."""
binary_sensors = []
ports = config.get(CONF_PORTS)
@@ -31,7 +31,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for port_addr, port_name in port.items():
binary_sensors.append(GC100BinarySensor(
port_name, port_addr, hass.data[DATA_GC100]))
add_devices(binary_sensors, True)
add_entities(binary_sensors, True)
class GC100BinarySensor(BinarySensorDevice):

View File

@@ -90,7 +90,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
data = HikvisionData(hass, url, port, name, username, password)
if data.sensors is None:
_LOGGER.error("Hikvision event stream has no data, unable to setup")
_LOGGER.error("Hikvision event stream has no data, unable to set up")
return False
entities = []

View File

@@ -13,13 +13,13 @@ DEVICETYPE_DEVICE_CLASS = {'motionsensor': 'motion',
'contactsensor': 'opening'}
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up Hive sensor devices."""
if discovery_info is None:
return
session = hass.data.get(DATA_HIVE)
add_devices([HiveBinarySensorEntity(session, discovery_info)])
add_entities([HiveBinarySensorEntity(session, discovery_info)])
class HiveBinarySensorEntity(BinarySensorDevice):

View File

@@ -5,31 +5,32 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.homematic/
"""
import logging
from homeassistant.const import STATE_UNKNOWN
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
from homeassistant.components.homematic import ATTR_DISCOVER_DEVICES, HMDevice
from homeassistant.const import STATE_UNKNOWN
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['homematic']
SENSOR_TYPES_CLASS = {
'Remote': None,
'ShutterContact': 'opening',
'MaxShutterContact': 'opening',
'IPShutterContact': 'opening',
'Smoke': 'smoke',
'SmokeV2': 'smoke',
'MaxShutterContact': 'opening',
'Motion': 'motion',
'MotionV2': 'motion',
'RemoteMotion': None,
'WeatherSensor': None,
'TiltSensor': None,
'PresenceIP': 'motion',
'Remote': None,
'RemoteMotion': None,
'ShutterContact': 'opening',
'Smoke': 'smoke',
'SmokeV2': 'smoke',
'TiltSensor': None,
'WeatherSensor': None,
}
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the HomeMatic binary sensor platform."""
if discovery_info is None:
return
@@ -39,7 +40,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
new_device = HMBinarySensor(conf)
devices.append(new_device)
add_devices(devices)
add_entities(devices)
class HMBinarySensor(HMDevice, BinarySensorDevice):

View File

@@ -1,55 +1,50 @@
"""
Support for HomematicIP binary sensor.
Support for HomematicIP Cloud binary sensor.
For more details about this component, please refer to the documentation at
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.homematicip_cloud/
"""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.homematicip_cloud import (
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
HMIPC_HAPID)
HMIPC_HAPID, HomematicipGenericDevice)
from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
DEPENDENCIES = ['homematicip_cloud']
_LOGGER = logging.getLogger(__name__)
ATTR_WINDOW_STATE = 'window_state'
ATTR_EVENT_DELAY = 'event_delay'
ATTR_MOTION_DETECTED = 'motion_detected'
ATTR_ILLUMINATION = 'illumination'
STATE_SMOKE_OFF = 'IDLE_OFF'
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the binary sensor devices."""
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Set up the HomematicIP Cloud binary sensor devices."""
pass
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up the HomematicIP binary sensor from a config entry."""
from homematicip.device import (ShutterContact, MotionDetectorIndoor)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the HomematicIP Cloud binary sensor from a config entry."""
from homematicip.aio.device import (
AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector)
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
devices = []
for device in home.devices:
if isinstance(device, ShutterContact):
if isinstance(device, AsyncShutterContact):
devices.append(HomematicipShutterContact(home, device))
elif isinstance(device, MotionDetectorIndoor):
elif isinstance(device, AsyncMotionDetectorIndoor):
devices.append(HomematicipMotionDetector(home, device))
elif isinstance(device, AsyncSmokeDetector):
devices.append(HomematicipSmokeDetector(home, device))
if devices:
async_add_devices(devices)
async_add_entities(devices)
class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
"""HomematicIP shutter contact."""
def __init__(self, home, device):
"""Initialize the shutter contact."""
super().__init__(home, device)
"""Representation of a HomematicIP Cloud shutter contact."""
@property
def device_class(self):
@@ -69,11 +64,7 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
"""MomematicIP motion detector."""
def __init__(self, home, device):
"""Initialize the shutter contact."""
super().__init__(home, device)
"""Representation of a HomematicIP Cloud motion detector."""
@property
def device_class(self):
@@ -86,3 +77,17 @@ class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
if self._device.sabotage:
return True
return self._device.motionDetected
class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice):
"""Representation of a HomematicIP Cloud smoke detector."""
@property
def device_class(self):
"""Return the class of this sensor."""
return 'smoke'
@property
def is_on(self):
"""Return true if smoke is detected."""
return self._device.smokeDetectorAlarmType != STATE_SMOKE_OFF

View File

@@ -26,7 +26,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a sensor for a Hydrawise device."""
hydrawise = hass.data[DATA_HYDRAWISE].data
@@ -45,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
hydrawise.controller_status.get('running', False)
sensors.append(HydrawiseBinarySensor(zone_data, sensor_type))
add_devices(sensors, True)
add_entities(sensors, True)
class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorDevice):

View File

@@ -30,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the IHC binary sensor platform."""
ihc_controller = hass.data[IHC_DATA][IHC_CONTROLLER]
info = hass.data[IHC_DATA][IHC_INFO]
@@ -56,7 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sensor_type, inverting)
devices.append(sensor)
add_devices(devices)
add_entities(devices)
class IHCBinarySensor(IHCDevice, BinarySensorDevice):

View File

@@ -2,15 +2,15 @@
Support for INSTEON dimmers via PowerLinc Modem.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.insteon_plm/
https://home-assistant.io/components/binary_sensor.insteon/
"""
import asyncio
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.insteon_plm import InsteonPLMEntity
from homeassistant.components.insteon import InsteonEntity
DEPENDENCIES = ['insteon_plm']
DEPENDENCIES = ['insteon']
_LOGGER = logging.getLogger(__name__)
@@ -23,28 +23,29 @@ SENSOR_TYPES = {'openClosedSensor': 'opening',
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the INSTEON PLM device class for the hass platform."""
plm = hass.data['insteon_plm'].get('plm')
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the INSTEON device class for the hass platform."""
insteon_modem = hass.data['insteon'].get('modem')
address = discovery_info['address']
device = plm.devices[address]
device = insteon_modem.devices[address]
state_key = discovery_info['state_key']
name = device.states[state_key].name
if name != 'dryLeakSensor':
_LOGGER.debug('Adding device %s entity %s to Binary Sensor platform',
device.address.hex, device.states[state_key].name)
new_entity = InsteonPLMBinarySensor(device, state_key)
new_entity = InsteonBinarySensor(device, state_key)
async_add_devices([new_entity])
async_add_entities([new_entity])
class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
class InsteonBinarySensor(InsteonEntity, BinarySensorDevice):
"""A Class for an Insteon device entity."""
def __init__(self, device, state_key):
"""Initialize the INSTEON PLM binary sensor."""
"""Initialize the INSTEON binary sensor."""
super().__init__(device, state_key)
self._sensor_type = SENSOR_TYPES.get(self._insteon_device_state.name)

View File

@@ -35,7 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the ISS sensor."""
if None in (hass.config.latitude, hass.config.longitude):
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
@@ -51,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
name = config.get(CONF_NAME)
show_on_map = config.get(CONF_SHOW_ON_MAP)
add_devices([IssBinarySensor(iss_data, name, show_on_map)], True)
add_entities([IssBinarySensor(iss_data, name, show_on_map)], True)
class IssBinarySensor(BinarySensorDevice):

View File

@@ -29,7 +29,7 @@ ISY_DEVICE_TYPES = {
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
add_entities: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 binary sensor platform."""
devices = []
devices_by_nid = {}
@@ -75,7 +75,7 @@ def setup_platform(hass, config: ConfigType,
for name, status, _ in hass.data[ISY994_PROGRAMS][DOMAIN]:
devices.append(ISYBinarySensorProgram(name, status))
add_devices(devices)
add_entities(devices)
def _detect_device_type(node) -> str:

View File

@@ -54,27 +54,27 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
async def async_setup_platform(hass, config, async_add_devices,
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up binary sensor(s) for KNX platform."""
if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, async_add_devices)
async_add_entities_discovery(hass, discovery_info, async_add_entities)
else:
async_add_devices_config(hass, config, async_add_devices)
async_add_entities_config(hass, config, async_add_entities)
@callback
def async_add_devices_discovery(hass, discovery_info, async_add_devices):
def async_add_entities_discovery(hass, discovery_info, async_add_entities):
"""Set up binary sensors for KNX platform configured via xknx.yaml."""
entities = []
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
device = hass.data[DATA_KNX].xknx.devices[device_name]
entities.append(KNXBinarySensor(hass, device))
async_add_devices(entities)
async_add_entities(entities)
@callback
def async_add_devices_config(hass, config, async_add_devices):
def async_add_entities_config(hass, config, async_add_entities):
"""Set up binary senor for KNX platform configured within platform."""
name = config.get(CONF_NAME)
import xknx
@@ -97,7 +97,7 @@ def async_add_devices_config(hass, config, async_add_devices):
entity.automations.append(KNXAutomation(
hass=hass, device=binary_sensor, hook=hook,
action=action, counter=counter))
async_add_devices([entity])
async_add_entities([entity])
class KNXBinarySensor(BinarySensorDevice):

View File

@@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['konnected']
async def async_setup_platform(hass, config, async_add_devices,
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up binary sensors attached to a Konnected device."""
if discovery_info is None:
@@ -31,7 +31,7 @@ async def async_setup_platform(hass, config, async_add_devices,
sensors = [KonnectedBinarySensor(device_id, pin_num, pin_data)
for pin_num, pin_data in
data[CONF_DEVICES][device_id][CONF_BINARY_SENSORS].items()]
async_add_devices(sensors)
async_add_entities(sensors)
class KonnectedBinarySensor(BinarySensorDevice):

View File

@@ -27,7 +27,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Linode droplet sensor."""
linode = hass.data.get(DATA_LINODE)
nodes = config.get(CONF_NODES)
@@ -40,7 +40,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return
dev.append(LinodeBinarySensor(linode, node_id))
add_devices(dev, True)
add_entities(dev, True)
class LinodeBinarySensor(BinarySensorDevice):

View File

@@ -13,7 +13,7 @@ from homeassistant.const import STATE_UNKNOWN
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Iterate through all MAX! Devices and add window shutters."""
devices = []
for handler in hass.data[DATA_KEY].values():
@@ -28,7 +28,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
MaxCubeShutter(handler, name, device.rf_address))
if devices:
add_devices(devices)
add_entities(devices)
class MaxCubeShutter(BinarySensorDevice):

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