Compare commits

..

163 Commits

Author SHA1 Message Date
Paulus Schoutsen
da3366859d Merge pull request #15570 from home-assistant/rc
0.74
2018-07-20 15:11:18 +02:00
Teemu R
200c0a8778 light.tplink: initialize min & max mireds only once, avoid i/o outside update (#15571)
* light.tplink: initialize min & max mireds only once, avoid i/o outside update

* revert the index change

* fix indent, sorry for overwriting your fix, balloob
2018-07-20 14:40:38 +02:00
Paulus Schoutsen
8e659baf25 Bumped version to 0.74.0 2018-07-20 12:44:15 +02:00
Jason Hu
2aa54ce22b Reset failed login attempts counter when login success (#15564) 2018-07-20 12:33:21 +02:00
Paulus Schoutsen
eff334a1d0 Remove relative time from state machine (#15560) 2018-07-20 12:32:45 +02:00
Paulus Schoutsen
b3bed7fb37 Allow auth providers to influence is_active (#15557)
* Allow auth providers to influence is_active

* Fix auth script test
2018-07-20 12:32:44 +02:00
Martin Hjelmare
61b3822374 Upgrade pymysensors to 0.16.0 (#15554) 2018-07-20 12:32:44 +02:00
Paulus Schoutsen
9fb04b5280 Update the frontend to 20180720.0 2018-07-20 12:30:32 +02:00
Paulus Schoutsen
7aa2a9e506 Bumped version to 0.74.0b4 2018-07-19 12:26:15 +02:00
Paulus Schoutsen
2fc0d83085 Allow CORS requests to token endpoint (#15519)
* Allow CORS requests to token endpoint

* Tests

* Fuck emulated hue

* Clean up

* Only cors existing methods
2018-07-19 12:26:08 +02:00
Paulus Schoutsen
ca0d4226aa Decouple emulated hue from http server (#15530) 2018-07-19 12:25:47 +02:00
Paulus Schoutsen
dff2e4ebc2 Don't be so strict client-side (#15546) 2018-07-19 12:23:14 +02:00
Jerad Meisner
9c337bc621 Added WS endpoint for changing homeassistant password. (#15527)
* Added WS endpoint for changing homeassistant password.

* Remove change password helper. Don't require current password.

* Restore current password verification.

* Added tests.

* Use correct send method
2018-07-19 12:23:14 +02:00
Paulus Schoutsen
5a1360678b Bump frontend to 20180719.0 2018-07-19 10:55:33 +02:00
Paulus Schoutsen
7b8ad64ba5 Bumped version to 0.74.0b3 2018-07-18 17:41:36 +02:00
Jason Hu
e64761b15e Disallow use insecure_example auth provider in configuration.yml (#15504)
* Disallow use insecure_example auth provider in configuration.yml

* Add unit test for auth provider config validate
2018-07-18 17:41:22 +02:00
Paulus Schoutsen
61273ff606 Bump frontend to 20180718.0 2018-07-18 17:34:28 +02:00
Paulus Schoutsen
5dc29bd2c3 Bumped version to 0.74.0b2 2018-07-17 10:59:07 +02:00
Paulus Schoutsen
20c316bce4 Bump frontend to 20180717.0 2018-07-17 10:58:58 +02:00
Matthew Garrett
8b475f45e9 Update HomeKit module code (#15502)
This fixes a bunch of bugs, including issues with concurrency in devices
that present multiple accessories, devices that insist on the TLV entries
being in the order that Apple use, and handling devices that send headers
and data in separate chunks. This should improve compatibility with
a whole bunch of HomeKit devices.
2018-07-17 10:58:51 +02:00
Paulus Schoutsen
a4318682f7 Add onboarding support (#15492)
* Add onboarding support

* Lint

* Address comments

* Mark user step as done if owner user already created
2018-07-17 10:58:51 +02:00
Paulus Schoutsen
a14d8057ed Add current user WS command (#15485) 2018-07-17 10:58:50 +02:00
Paulus Schoutsen
55f8b0a2f5 Bumped version to 0.74.0b1 2018-07-16 22:14:51 +02:00
Paulus Schoutsen
bb37300a48 Merge branch 'master' into rc 2018-07-16 22:14:45 +02:00
Paulus Schoutsen
0f12b37977 0.73.2 - security release (#15494)
* Extract SSL context creation to helper (#15483)

* Extract SSL context creation to helper

* Lint

* Bumped version to 0.73.2
2018-07-16 22:13:12 +02:00
Paulus Schoutsen
7f18739267 Bumped version to 0.74.0b0 2018-07-16 08:51:52 +02:00
Paulus Schoutsen
a1b478b3ac Version bump to 0.74.0.dev0 2018-07-16 08:51:37 +02:00
Paulus Schoutsen
edf1f44668 Bump frontend to 20180716.0 2018-07-16 08:50:21 +02:00
Anders Melchiorsen
60f780cc37 Update limitlessled to 1.1.2 (#15481) 2018-07-15 23:24:35 +02:00
Anders Melchiorsen
7d0cc7e26c Fix flux_led turning on with color or effect (#15472) 2018-07-15 23:18:52 +02:00
Paulus Schoutsen
864a254071 Aware comments (#15480)
* Make sure we cannot deactivate the owner

* Use different error code when trying to fetch token for inactive user
2018-07-15 23:09:05 +02:00
Andrey
5995c6a2ac Switch to own packaged version of pygtfs (#15040) 2018-07-15 21:32:20 +02:00
Paulus Schoutsen
ed0cfc4f31 Add user via cmd line creates owner (#15470)
* Add user via cmd line creates owner

* Ensure access tokens are not verified for inactive users

* Stale print

* Lint
2018-07-15 20:46:15 +02:00
Mattias Welponer
6db069881b Update homematicip_cloud with enum states (#15460)
* Update to next version with enum states

* Change to generic dimmer class

* Update of requirement files

* Update to hmip lib to v0.9.7

* Missing update of requirements files

* Cleanup of icon properties
2018-07-15 02:59:19 +02:00
huangyupeng
ca4f69f557 Add Tuya light platform (#15444)
* add tuya light platform

* fix as review required
2018-07-15 02:48:32 +02:00
Ville Skyttä
37ccf87516 Remove unnecessary executable permissions (#15469) 2018-07-14 23:03:36 +02:00
Tom Harris
201c9fed77 Implement is_on (#15459)
* Implement is_on

* Remove var
2018-07-14 11:04:00 +02:00
Daniel Perna
3b5775573b Add IPPassageSensor (HmIP-SPDR) (#15458) 2018-07-14 02:31:51 +02:00
Jason Antman
6e22a0e4d9 Fix ZWave RGBW lights not producing color without explicit white_value (#15412)
* Fix ZWave RGBW lights not producing color without explicit white_value (#13930)

* simplify conditional in previous commit (#13930)

* ZwaveColorLight - only zero _white if white_value not specified in call (#13930)
2018-07-14 00:54:15 +02:00
Mattias Welponer
ce5b4cd51e Add HomematicIP Cloud dimmer light device (#15456)
* Add dimmable light device

* Add imports

* Fix float and int conversion
2018-07-13 23:25:11 +02:00
Paulus Schoutsen
538236de8f Fix formatting pylint comments in test (#15450) 2018-07-13 23:02:23 +02:00
Fabian Affolter
1007bb83aa Upgrade keyring to 13.2.1 (#15453) 2018-07-13 20:02:13 +02:00
Fabian Affolter
79955a5785 Catch the ValueError if the bulb was in the wrong mode (#15434) 2018-07-13 20:01:57 +02:00
Andrey
e60f9ca392 More typing (#15449)
## Description:

More typing improvements.

Switch to using `mypy.ini` for flexibility

Add `warn_return_any` check except in `homeassistant.util.yaml` that does typing hacks. Fix some type annotations as resulting from this check and ignore others were fixing is hard.

## Checklist:
  - [x] The code change is tested and works locally.
  - [x] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
2018-07-13 20:14:45 +03:00
Paulus Schoutsen
ae581694ac Bump frontend to 20180713.0 2018-07-13 15:33:33 +02:00
Paulus Schoutsen
70fe463ef0 User management (#15420)
* User management

* Lint

* Fix dict

* Reuse data instance

* OrderedDict all the way
2018-07-13 15:31:20 +02:00
Paulus Schoutsen
84858f5c19 Fix comment formatting (#15447) 2018-07-13 15:05:55 +02:00
Ville Skyttä
a6ba5ec1c8 upgrade-mypy (#14904)
* Upgrade mypy to 0.600

* Upgrade mypy to 0.610

* Typing improvements

* remove unneeded or

* remove merge artifact

* Update loader.py
2018-07-13 13:49:24 +02:00
Andrey
c2fe0d0120 Make typing checks more strict (#14429)
## Description:

Make typing checks more strict: add `--strict-optional` flag that forbids implicit None return type. This flag will become default in the next version of mypy (0.600)

Add `homeassistant/util/` to checked dirs.

## Checklist:
  - [x] The code change is tested and works locally.
  - [x] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
2018-07-13 13:24:51 +03:00
Paulus Schoutsen
b6ca03ce47 Reorg auth (#15443) 2018-07-13 11:43:08 +02:00
Andrey
23f1b49e55 Add python 3.8-dev to travis and tox (#15347)
* Add Python 3.8-dev tox tests.

* Allow failures on 3.8-dev

* Allow failures on 3.8-dev take2

* Only run on pushes to dev
2018-07-13 11:37:03 +02:00
Jason Hu
6e3ec97acf Include request.path in legacy api password warning message (#15438) 2018-07-13 09:19:13 +02:00
Mattias Welponer
4a6afc5614 Add HomematicIP alarm control panel (#15342)
* Add HomematicIP security zone

* Update access point tests

* Fix state if not armed and coments

* Add comment for the empty state_attributes

* Fix comment

* Fix spelling
2018-07-13 03:57:41 +02:00
Anders Melchiorsen
b557c17f76 Make LimitlessLED color/temperature attributes mutually exclusive (#15298) 2018-07-12 17:17:00 +02:00
Matthew Garrett
c587536547 Ignore some HomeKit devices (#15316)
There are some devices that speak HomeKit that we shouldn't expose. Some
bridges (such as the Hue) provide reduced functionality over HomeKit and
have a functional native API, so should be ignored. We also shouldn't
offer to configure the built-in Home Assistant HomeKit bridge.
2018-07-12 11:52:37 +02:00
Daniel Perna
4c6394b307 Fix HomeMatic variables (#15417)
* Update __init__.py

* Update requirements_all.txt
2018-07-12 11:49:39 +02:00
Paulus Schoutsen
534233388c Merge branch 'master' into dev 2018-07-12 11:47:10 +02:00
huangyupeng
43b31e88ba Add Tuya component and switch support (#15399)
* support for tuya platform

* support tuya platform

* lint fix

* change dependency

* add tuya platform support

* remove tuya platform except switch. fix code as required

* fix the code as review required

* fix as required

* fix a mistake
2018-07-12 10:19:35 +02:00
Marcelo Moreira de Mello
6197fe0121 Change Ring binary_sensor frequency polling to avoid rate limit exceeded errors (#15414) 2018-07-11 09:27:22 +02:00
Paulus Schoutsen
1f6331c69d Fix credentials lookup (#15409) 2018-07-10 20:33:03 +02:00
Philipp Schmitt
fd568d77c7 Fix liveboxplaytv empty channel list (#15404) 2018-07-10 15:51:37 +02:00
Anders Melchiorsen
f32098abe4 Fix confused brightness of xiaomi_aqara gateway light (#15314) 2018-07-10 13:26:42 +02:00
Joakim Sørensen
b65d7daed8 removed unused return (#15402) 2018-07-10 13:19:32 +02:00
Giuseppe
9ea0c409e6 Improve NetAtmo sensors update logic (#14866)
* Added a "last update" sensor that could be used by automations + cosmetic changes

* Improved the update logic of sensor data

The platform is now continuously adjusting the refresh interval
in order to synchronize with the expected next update from the
NetAtmo cloud. This significantly improves reaction time of
automations while keeping the refresh time to the recommended
value (10 minutes).

* Linting

* Incorporated the advanced Throttle class to support adaptive
throttling, as opposed to integrating it in the core framework.

Following code review, it was suggested to implement the
specialised Throttle class in this platform instead of making a
change in the general util package. Except that the required change
(about 4 LoC) is part of the only relevant piece of code of that
class, therefore this commit includes a full copy of the Throttle
class from homeassistant.util, plus the extra feature to support
adaptive throttling.

* Cosmetic changes on the introduced "last updated" sensor

* Alternate implementation for the adaptive throttling

Ensure the updates from the cloud are throttled and adapted to the
last update time provided by NetAtmo, without using the Throttle
decorator. Similar logic and similar usage of a lock to protect
the execution of the remote update.

* Linting
2018-07-10 12:30:48 +02:00
Paulus Schoutsen
2ee62b10bc Bump frontend to 20180710.0 2018-07-10 12:01:52 +02:00
Paulus Schoutsen
dbdd0a1f56 Expire auth code after 10 minutes (#15381) 2018-07-10 11:20:22 +02:00
Robin
df8c59406b Add Facebox teach service (#14998)
* Adds service

* Address pylint

* Update facebox.py

* patch tests

* Update facebox.py

* Update test_facebox.py

* Update facebox.py

* Update facebox.py

* Update facebox.py

* Update test_facebox.py

* Update test_facebox.py

* Update facebox.py

* Update facebox.py

* Update facebox.py

* Update facebox.py

* Adds total_matched_faces

* Update test_facebox.py

* Update facebox.py

* Update test_facebox.py

* Update test_facebox.py

* Remove fixtures

Removes the fixtures which were causing `setup` to fail, replace with `@patch`

* Fix teach service test and lint issues
2018-07-10 03:11:39 +02:00
Joakim Sørensen
c5a2ffbcb9 Add Cloudflare DNS component. (#15388)
* Add Cloudflare DNS component

* Removed man

* Update .coveragerc

* Update cloudflare.py

* Update cloudflare.py

* Changed records to be required

* Fix typos, update order and other minor changes
2018-07-09 23:11:54 +02:00
hanzoh
e62bb299ff Add new voices to Amazon Polly (#15320) 2018-07-09 23:01:17 +02:00
Daniel Perna
6ee8d9bd65 Update ha-philipsjs to 0.0.5 (#15378)
* Update requirements_all.txt

* Update philips_js.py
2018-07-09 21:35:06 +02:00
Ville Skyttä
14a34f8c4b Remove some unneeded pylint import-error disables (#15386) 2018-07-09 21:34:27 +02:00
Ville Skyttä
3b93fa80be Add httplib2 to h.c.google requirements (#15385) 2018-07-09 21:33:58 +02:00
Paulus Schoutsen
57977bcef3 Bump frontend to 20180709.0 2018-07-09 18:26:51 +02:00
Paulus Schoutsen
0d4841cbea Use IndieAuth for client ID (#15369)
* Use IndieAuth for client ID

* Lint

* Lint & Fix tests

* Allow local IP addresses

* Update comment
2018-07-09 18:24:46 +02:00
Fabian Affolter
f7d7d825b0 Efergy (#15380)
* Update format

* Use string formatting
2018-07-09 14:39:28 +02:00
iliketoprogram14
1d1408b98d Fixed issue 15340. alexa/smart_home module can now skip properties that aren't supported in the current state, eg lowerSetpoint in Heat mode or targetSetpoint in Eco mode for Nest devices. (#15352) 2018-07-09 11:44:50 +02:00
starkillerOG
b9eb0081cd Add sound mode support (#14910)
* Add sound mode support

* continuation line indent

* indentation

* indentation

* Remove option to configure sound_mode_dict

* Sound mode support

- removed the sound_mode_raw propertie because it was not used, (still available through self._sound_mode_raw (as device attribute for automations and diagnostics)

* Detect sound mode support from device

Removed the config option to indicate if sound mode is supported.
Added detection if sound mode is supported from the receiver itself.
Pushed denonavr library to V.0.7.4

* Pushed denonavr to v.0.7.4
2018-07-09 11:39:41 +02:00
Paul Klingelhuber
287b1bce15 Add support for multi-channel enocean switches (D2-01-12 profile) (#14548) 2018-07-09 11:05:25 +02:00
Diogo Gomes
ec3d2e97e8 fix camera.push API overwrite (#15334)
* fix camera.push API overwrite

* dont search in the component dictionary, but in hour own

* remove error message

* hound
2018-07-09 11:04:51 +02:00
Mattias Welponer
1ff329d9d6 Add HomematicIP Cloud light power consumption and energie attributes (#15343)
* Add power consumption and energie attributes

* Fix lint

* Change attribute name and include kwh
2018-07-09 05:37:59 +02:00
sjabby
703d71c064 Frontend: Allow overriding default url when added to home screen (#15368)
Frontend: Allow overriding default url when added to home screen
2018-07-08 22:45:01 +02:00
Paulus Schoutsen
a2a4c633f3 Merge branch 'rc' 2018-07-08 17:35:59 +02:00
Paulus Schoutsen
e6dd4f6e13 Bumped version to 0.73.1 2018-07-08 17:35:30 +02:00
Paulus Schoutsen
b327ea2023 Bump frontend to 20180708.0 2018-07-08 17:31:03 +02:00
Paulus Schoutsen
b333dba875 Bump frontend to 20180708.0 2018-07-08 17:25:15 +02:00
Andrey
02238b6412 Add python 3.7 to travis and tox (#14523)
* Add python 3.7 to travis and tox

* Use pyyaml from github

* Don't version constraints

* Fix version tag

* Change to new pyyaml release

* Python 3.7 requires xenial

* Fix namespace detection

* Use correct RegEx type

* Update pexpect to 4.6

* Use correct validation for dictionaries

* Disable Py37 incompatible packages

* Upgrade all pexpect to 4.6

* Add explicit None as default param
2018-07-07 10:48:02 -04:00
Tommy Jonsson
bd62248841 Add original message as dialogflow_query parameter (#15304)
So you can access for example sessionId as {{ dialogflow_query.sessionId }} in intent templates.
2018-07-07 11:10:43 +02:00
Ville Skyttä
dabbd7bd63 Upgrade pytest to 3.6.3 (#15332) 2018-07-07 11:06:49 +02:00
Fabian Affolter
b5c7afcf75 Upgrade keyring to 13.2.0 (#15322) 2018-07-07 11:06:00 +02:00
Fabian Affolter
f8f8da959a Upgrade youtube_dl to 2018.07.04 (#15323) 2018-07-07 11:05:44 +02:00
Mattias Welponer
9970965718 Add HomematicIP Cloud Config Flow and Entries loading (#14861)
* Add HomematicIP Cloud to config flow

* Inititial trial for config_flow

* Integrations text files

* Load and write config_flow and init homematicip_cloud

* Split into dedicated files

* Ceanup of text messages

* Working config_flow

* Move imports inside a function

* Enable laoding even no accesspoints are defined

* Revert unnecassary changes in CONFIG_SCHEMA

* Better error handling

* fix flask8

* Migration to async for token generation

* A few fixes

* Simplify config_flow

* Bump version to 9.6 with renamed package

* Requirements file

* First fixes after review

* Implement async_step_import

* Cleanup for Config Flow

* First tests for homematicip_cloud setup

* Remove config_flow tests

* Really remove all things

* Fix comment

* Update picture

* Add support for async_setup_entry to switch and climate platform

* Update path of the config_flow picture

* Refactoring for better tesability

* Further tests implemented

* Move 3th party lib inside function

* Fix lint

* Update requirments_test_all.txt file

* UPdate of requirments_test_all.txt did not work

* Furder cleanup in websocket connection

* Remove a test for the hap

* Revert "Remove a test for the hap"

This reverts commit 968d58cba1.

* First tests implemented for config_flow

* Fix lint

* Rework of client registration process

* Implemented tests for config_flow 100% coverage

* Cleanup

* Cleanup comments and code

* Try to fix import problem

* Add homematicip to the test env requirements
2018-07-06 17:05:34 -04:00
Paulus Schoutsen
a1d8b0e9b3 Merge pull request #15330 from home-assistant/rc
0.73
2018-07-06 16:41:09 -04:00
Paulus Schoutsen
1e7cfc04af Bumped version to 0.73.0 2018-07-06 22:31:09 +02:00
Luke Fritz
0f1bcfd63b Add additional sensors for Arlo Baby camera (#15074)
* Add additional sensors for Arlo Baby camera

* Fix linter errors

* Fix linter error

* Add tests for Arlo sensors

* Fix linter errors

* Bump pyarlo dependency to 0.1.9

* Remove unnecessary AttributeError except

* Fix module reference error in py35

* Fix test

* Address PR review concerns

* Convert to standalone pytest methods

* Fix linter errors

* Fix linter errors

* Fix linter errors

* Fix test

* Remove redundant check, fix async test

* Fix linter error

* Added check for total_cameras sensor, added additional attribute tests

* Add missing docstring
2018-07-06 10:26:03 +02:00
Aaron Bach
f65c3940ae Fix exception when parts of Pollen.com can't be reached (#15296)
Fix exception when parts of Pollen.com can't be reached
2018-07-04 17:30:15 -06:00
Paulus Schoutsen
46de89e1a3 Bumped version to 0.73.0b6 2018-07-04 12:11:52 -04:00
Paulus Schoutsen
852526e10a Bump frontend to 20180704.0 2018-07-04 12:11:39 -04:00
Paulus Schoutsen
91d6d0df84 Bump frontend to 20180704.0 2018-07-04 12:11:24 -04:00
Paulus Schoutsen
cb129bd207 Add system generated users (#15291)
* Add system generated users

* Fix typing
2018-07-04 11:50:08 -04:00
Marcelo Moreira de Mello
a6e9dc81aa Added support to HTTPS URLs on SynologyDSM (#15270)
* Added support to HTTPS URLs on SynologyDSM

* Bumped python-synology to 0.1.1

* Makes lint happy

* Added attribution to Synology and fixed 3rd library version

* Fixed requirements_all.txt

* Makes SynologyDSM defaults to 5001 using SSL
2018-07-04 07:46:01 +02:00
Diogo Gomes
5f7ac09a74 Added Push Camera (#15151)
* Added push camera

* add camera.push

* Address comments and add tests

* auff auff

* trip time made no sense

* travis lint

* Mock dependency

* hound

* long line

* long line

* better mocking

* remove blank image

* no more need to mock dependency

* remove import

* cleanup

* no longer needed

* unused constant

* address @pvizeli review

* add force_update

* Revert "add force_update"

This reverts commit e203785ea8.

* rename parameter
2018-07-04 07:44:47 +02:00
cdce8p
42775142f8 Fix yeelight light brightness integer (#15290) 2018-07-03 20:50:13 -06:00
Aaron Bach
2525fc52b3 Update Tile platform to be async (#15073)
* Updated

* Updated requirements

* Added expired session handling

* Changes

* Member-requested changes

* Bump to 2.0.2

* Bumping requirements

* Better exception handling and tidying

* Move asyncio stuff to HASS built-ins

* Revising re-initi

* Hound

* Hound
2018-07-03 20:41:54 -06:00
Paulus Schoutsen
07dde62e70 Bumped version to 0.73.0b5 2018-07-03 14:58:31 -04:00
Paulus Schoutsen
cb458b7745 Bump frontend to 20180703.1 2018-07-03 14:58:27 -04:00
Paulus Schoutsen
b2df199674 Bump frontend to 20180703.1 2018-07-03 14:51:57 -04:00
Paulus Schoutsen
857c58c4b7 Disable the calendar panel (#15282) 2018-07-03 13:20:42 -04:00
Paulus Schoutsen
b82371f44b Bumped version to 0.73.0b4 2018-07-03 11:11:14 -04:00
Paulus Schoutsen
1c525968d1 Bump frontend to 20180703.0 2018-07-03 11:10:07 -04:00
Paulus Schoutsen
5ec61e4649 Bump frontend to 20180703.0 2018-07-03 11:03:23 -04:00
Andrey
184d0a99c0 Switch to own packaged version of suds-passworddigest (#15261) 2018-07-03 12:43:24 +02:00
Fabian Affolter
232f56de62 Add support for new API (fixes #14911) (#15279) 2018-07-03 12:30:56 +02:00
Diogo Gomes
66e33c7979 Merge pull request #13390 from nielstron/filter-band-pass
Adds a range filter to the Filter Sensor
2018-07-03 11:01:34 +01:00
nielstron
6420ab5535 Remove default none from filter sensor 2018-07-03 11:06:52 +02:00
Fabian Affolter
ed3fe1cc6f Add isort configuration (#15278) 2018-07-03 09:47:14 +02:00
pepeEL
cd1cfd7e8e New device to support option MY in somfy (#15272)
New device to support option MY in somfy
2018-07-03 08:39:42 +02:00
Paul Stenius
31e23ebae2 expose climate current temperature in prometeus metrics (#15232)
* expose climate current temperature in prometeus metrics

* import ATTR_CURRENT_TEMPERATURE from climate instead of const

* remove duplicated ATTR_CURRENT_TEMPERATURE from const

* fix ATTR_CURRENT_TEMPERATURE import
2018-07-02 18:03:46 -04:00
nielstron
fb65276daf Remove math.inf as bounds 2018-07-03 00:00:46 +02:00
Robert Svensson
bedd2d7e41 deCONZ - new sensor attribute 'on' and new sensor GenericFlag (#15247)
* New sensor attribute 'on'
* New sensor GenericFlag
2018-07-02 23:14:38 +02:00
Fabian Affolter
120111ceee Upgrade keyring to 13.1.0 (#15268) 2018-07-02 23:03:56 +02:00
shker
e6390b8e41 Fix python-miio 0.4 compatibility of the xiaomi miio device tracker (#15244) 2018-07-02 22:33:40 +02:00
Paulus Schoutsen
0feb4c5439 Bump frontend to 20180702.1 2018-07-02 14:43:31 -04:00
Diogo Gomes
f3588a8782 Update image_processing async (#15082)
* scan() -> async_job

* added async_scan
2018-07-02 16:57:52 +02:00
William Scanlon
2145ac5e46 Added support for Duke Energy smart meters (#15165)
* Added support for Duke Energy smart meters

* Fixed hound

* Added function docstring

* Moved strings to constants, implemented unique_id, and cleaned up setup.

* Added doc string.

* Fixed review issues.

* Updated pydukenergy to 0.0.6 and set update interval to 2 hours

* Updated requirements_all
2018-07-02 16:55:34 +02:00
Paulus Schoutsen
00c366d7ea Update frontend to 20180702.0 2018-07-02 08:56:37 -04:00
Paulus Schoutsen
dd59054003 Update translations 2018-07-02 08:53:33 -04:00
David Worsham
36f566a529 Fix Roomba exception (#15262)
* Fix Roomba exception

* Switch to single quotes
2018-07-02 14:12:25 +02:00
Ville Skyttä
4d93a9fd38 Pass tox posargs to pylint (#15226) 2018-07-02 12:47:20 +03:00
Klaudiusz Staniek
d3df96a8de Added setting cover tilt position in scene (#15255)
## Description:
This feature adds possibly of setting tilt_position in scene for covers.

**Related issue (if applicable):** fixes #<home-assistant issue number goes here>

**Pull request in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) with documentation (if applicable):** home-assistant/home-assistant.github.io#<home-assistant.github.io PR number goes here>

## Example entry for `configuration.yaml` (if applicable):
```yaml
scene:
  - name: Close Cover Tilt
    entities:
      cover.c_office_north:
        tilt_position: 0

  - name: Open Cover Tilt
    entities:
      cover.c_office_north:
        tilt_position: 100
```

## Checklist:
  - [x] The code change is tested and works locally.
  - [x] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**

If user exposed functionality or configuration variables are added/changed:
  - [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)

If the code communicates with devices, web services, or third-party tools:
  - [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
  - [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
  - [ ] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.
  - [ ] New files were added to `.coveragerc`.

If the code does not interact with devices:
  - [ ] Tests have been added to verify that the new code works.

[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L14
[ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L54
2018-07-02 12:44:36 +03:00
Andrey
6c77702dcc Switch to own packaged version of pylgnetcast (#15042)
## Description:

Switch to own packaged version of pylgnetcast

Request to make a pypi package didn't get any response: https://github.com/wokar/pylgnetcast/issues/1

**Related issue (if applicable):** #7069
2018-07-02 10:57:26 +03:00
Steven Conaway
86165750ff Fix typo in Docker files (#15256) 2018-07-02 07:02:09 +02:00
Jason Hu
a64a66dd62 Only create front-end client_id once (#15214)
* Only create frontend client_id once

* Check user and client_id before create refresh token

* Lint

* Follow code review comment

* Minor clenaup

* Update doc string
2018-07-01 13:36:50 -04:00
Anders Melchiorsen
dffe36761d Make LIFX color/temperature attributes mutually exclusive (#15234) 2018-07-01 13:06:30 -04:00
Jason Hu
0a186650bf Fix an issue when user's nest developer account don't have permission (#15237) 2018-07-01 13:04:12 -04:00
Fabian Affolter
6c77c9d372 Upgrade WazeRouteCalculator to 0.6 (#15251) 2018-07-01 13:02:02 -04:00
Fabian Affolter
4a4b9180d8 Upgrade sqlalchemy to 1.2.9 (#15250) 2018-07-01 13:01:48 -04:00
Paulus Schoutsen
235282e335 Bump frontend to 20180701.0 2018-07-01 13:00:34 -04:00
Ville Skyttä
6f582dcf24 Lint cleanups (#15243)
* Remove some unused imports

* Fix a flake8 E271
2018-07-01 11:57:01 -04:00
Andy Castille
9db8759317 Rachio webhooks (#15111)
* Make fewer requests to the Rachio API

* BREAKING: Rewrite Rachio component
2018-07-01 11:54:51 -04:00
David Thulke
136cc1d44d allow extra slot values in intents (#15246) 2018-07-01 11:51:40 -04:00
cdce8p
4c258ce08b Revert some changes to setup.py (#15248) 2018-07-01 11:48:54 -04:00
Leonardo Brondani Schenkel
3c04b0756f deconz: proper fix light.turn_off with transition (#15227)
Previous commit d4f7dfa successfully fixed the bug in which lights
would not turn off if a transition was specified, however if 'bri' is not
present in the payload of the PUT request set to deCONZ, then any
'transitiontime' ends up being ignored. This commit addresses the
unintended side effect by reintroducing 'bri', resulting in the following
payload:

{ "on": false, "bri": 0, "transitiontime": ... }
2018-07-01 12:32:48 +02:00
Yevgeniy
c0229ebb77 Add precipitations to Openweathermap daily forecast mode (#15240)
* Add precipitations to daily forecast mode

* Remove line breaks
2018-07-01 11:54:24 +02:00
Ville Skyttä
cfe7c0aa01 Upgrade pytest to 3.6.2 (#15241) 2018-07-01 10:40:23 +02:00
Jason Hu
f874efb224 By default to use access_token if hass.auth.active (#15212)
* Force to use access_token if hass.auth.active

* Not allow Basic auth with api_password if hass.auth.active

* Block websocket api_password auth when hass.auth.active

* Add legacy_api_password auth provider

* lint

* lint
2018-06-30 22:31:36 -04:00
cdce8p
3da4642194 Use async syntax for cover platforms (#15230) 2018-06-30 18:10:59 +02:00
Fabian Affolter
0aad056ca7 Fix typos (#15233) 2018-06-30 17:12:00 +02:00
Carl Chan
c5ceb40598 Add additional parameters to NUT UPS sensor (#15066)
* Update nut.py

Added input.frequency and a number of output parameters.

* Update nut.py

Fixed formatting issues
Added "devices" fields

* Separated "device.description" line to two lines.

* Update nut.py

Removed device.* sensors
2018-06-30 14:57:48 +02:00
pepeEL
27a37e2013 Add new RTS device (#15116)
* Add new RTS device

Add new RTS Somfy device as cover-ExteriorVenetianBlindRTSComponent

* add next device

add next device
2018-06-30 14:56:43 +02:00
Leonardo Brondani Schenkel
10d1e81f10 deconz: fix light.turn_off with transition (#15222)
When light.turn_off is invoked with a transition, the following payload was
sent to deCONZ via PUT to /light/N/state:

{ "bri": 0, "transitiontime": transition }

However, on recent versions of deCONZ (latest is 2.05.31 at the time of
writing) this does not turn off the light, just sets it to minimum brightness
level (brightness is clamped to minimum level the light supports without
turning it off).

This commit makes the code send this payload instead:

{ "on": false, "transitiontime": transition }

This works as intended and the light does transition to the 'off' state.
This change was tested with Philips Hue colored lights, IKEA colored lights
and IKEA white spectrum lights: they were all able to be turned off
successfully with the new payload, and none of them could be turned off with
the old payload.
2018-06-30 00:59:10 +02:00
nielstron
33990badcd Fixed Rangefilter constructor call 2018-06-06 20:40:56 +02:00
nielstron
8061f15aec Removal of windows size and precision for range filter 2018-06-06 20:40:55 +02:00
nielstron
25f7c31911 Fixed wrong bound assignment on values below the lower bound 2018-06-06 20:40:54 +02:00
nielstron
bb98331ba4 Fix doctring newline and handle ha.state string-being 2018-06-06 20:40:53 +02:00
nielstron
07d139b3a8 Fix wrong comparison 2018-06-06 20:40:53 +02:00
nielstron
f4ef8fd1bc Changes for new FilterState construct 2018-06-06 20:40:52 +02:00
nielstron
ba836c2e36 Fix indent 2018-06-06 20:40:52 +02:00
nielstron
a0ab356936 Renamed to range filter 2018-06-06 20:40:51 +02:00
nielstron
734a83c657 Removed default values and fixed description in sensor.filter 2018-06-06 20:40:50 +02:00
nielstron
b42f4012d1 Fixed test 2018-06-06 20:40:50 +02:00
nielstron
8501312292 Reordered attribute order 2018-06-06 20:40:49 +02:00
nielstron
3faed2edc1 Add test for new band_pass filter 2018-06-06 20:40:49 +02:00
nielstron
bc70619b17 Added bandpass filter
Allows values in a given range
2018-06-06 20:40:48 +02:00
233 changed files with 6485 additions and 2213 deletions

View File

@@ -64,6 +64,8 @@ omit =
homeassistant/components/cast/*
homeassistant/components/*/cast.py
homeassistant/components/cloudflare.py
homeassistant/components/comfoconnect.py
homeassistant/components/*/comfoconnect.py
@@ -341,6 +343,9 @@ omit =
homeassistant/components/zoneminder.py
homeassistant/components/*/zoneminder.py
homeassistant/components/tuya.py
homeassistant/components/*/tuya.py
homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/canary.py
homeassistant/components/alarm_control_panel/concord232.py
@@ -612,6 +617,7 @@ omit =
homeassistant/components/sensor/domain_expiry.py
homeassistant/components/sensor/dte_energy_bridge.py
homeassistant/components/sensor/dublin_bus_transport.py
homeassistant/components/sensor/duke_energy.py
homeassistant/components/sensor/dwd_weather_warnings.py
homeassistant/components/sensor/ebox.py
homeassistant/components/sensor/eddystone_temperature.py

2
.isort.cfg Normal file
View File

@@ -0,0 +1,2 @@
[settings]
multi_line_output=4

View File

@@ -16,11 +16,17 @@ matrix:
env: TOXENV=py35
- python: "3.6"
env: TOXENV=py36
# - python: "3.6-dev"
# env: TOXENV=py36
# allow_failures:
# - python: "3.5"
# env: TOXENV=typing
- python: "3.7"
env: TOXENV=py37
dist: xenial
- python: "3.8-dev"
env: TOXENV=py38
dist: xenial
if: branch = dev AND type = push
allow_failures:
- python: "3.8-dev"
env: TOXENV=py38
dist: xenial
cache:
directories:

View File

@@ -241,7 +241,7 @@ def cmdline() -> List[str]:
def setup_and_run_hass(config_dir: str,
args: argparse.Namespace) -> Optional[int]:
args: argparse.Namespace) -> int:
"""Set up HASS and run."""
from homeassistant import bootstrap
@@ -274,7 +274,7 @@ def setup_and_run_hass(config_dir: str,
log_no_color=args.log_no_color)
if hass is None:
return None
return -1
if args.open_ui:
# Imported here to avoid importing asyncio before monkey patch

View File

@@ -1,670 +0,0 @@
"""Provide an authentication layer for Home Assistant."""
import asyncio
import binascii
import importlib
import logging
import os
import uuid
from collections import OrderedDict
from datetime import datetime, timedelta
import attr
import voluptuous as vol
from voluptuous.humanize import humanize_error
from homeassistant import data_entry_flow, requirements
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID
from homeassistant.core import callback
from homeassistant.util import dt as dt_util
from homeassistant.util.decorator import Registry
_LOGGER = logging.getLogger(__name__)
STORAGE_VERSION = 1
STORAGE_KEY = 'auth'
AUTH_PROVIDERS = Registry()
AUTH_PROVIDER_SCHEMA = vol.Schema({
vol.Required(CONF_TYPE): str,
vol.Optional(CONF_NAME): str,
# Specify ID if you have two auth providers for same type.
vol.Optional(CONF_ID): str,
}, extra=vol.ALLOW_EXTRA)
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
DATA_REQS = 'auth_reqs_processed'
def generate_secret(entropy: int = 32) -> str:
"""Generate a secret.
Backport of secrets.token_hex from Python 3.6
Event loop friendly.
"""
return binascii.hexlify(os.urandom(entropy)).decode('ascii')
class AuthProvider:
"""Provider of user authentication."""
DEFAULT_TITLE = 'Unnamed auth provider'
initialized = False
def __init__(self, hass, store, config):
"""Initialize an auth provider."""
self.hass = hass
self.store = store
self.config = config
@property
def id(self): # pylint: disable=invalid-name
"""Return id of the auth provider.
Optional, can be None.
"""
return self.config.get(CONF_ID)
@property
def type(self):
"""Return type of the provider."""
return self.config[CONF_TYPE]
@property
def name(self):
"""Return the name of the auth provider."""
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
async def async_credentials(self):
"""Return all credentials of this provider."""
return await self.store.credentials_for_provider(self.type, self.id)
@callback
def async_create_credentials(self, data):
"""Create credentials."""
return Credentials(
auth_provider_type=self.type,
auth_provider_id=self.id,
data=data,
)
# Implement by extending class
async def async_initialize(self):
"""Initialize the auth provider.
Optional.
"""
async def async_credential_flow(self):
"""Return the data flow for logging in with auth provider."""
raise NotImplementedError
async def async_get_or_create_credentials(self, flow_result):
"""Get credentials based on the flow result."""
raise NotImplementedError
async def async_user_meta_for_credentials(self, credentials):
"""Return extra user metadata for credentials.
Will be used to populate info when creating a new user.
"""
return {}
@attr.s(slots=True)
class User:
"""A user."""
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)
name = attr.ib(type=str, default=None)
# List of credentials of a user.
credentials = attr.ib(type=list, default=attr.Factory(list), cmp=False)
# Tokens associated with a user.
refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict), cmp=False)
@attr.s(slots=True)
class RefreshToken:
"""RefreshToken for a user to grant new access tokens."""
user = attr.ib(type=User)
client_id = attr.ib(type=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
@attr.s(slots=True)
class Credentials:
"""Credentials for a user on an auth provider."""
auth_provider_type = attr.ib(type=str)
auth_provider_id = attr.ib(type=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)
@attr.s(slots=True)
class Client:
"""Client that interacts with Home Assistant on behalf of a user."""
name = attr.ib(type=str)
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
secret = attr.ib(type=str, default=attr.Factory(generate_secret))
redirect_uris = attr.ib(type=list, default=attr.Factory(list))
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
return module
async def auth_manager_from_config(hass, provider_configs):
"""Initialize an auth manager from config."""
store = AuthStore(hass)
if provider_configs:
providers = await asyncio.gather(
*[_auth_provider_from_config(hass, store, config)
for config in provider_configs])
else:
providers = []
# So returned auth providers are in same order as config
provider_hash = OrderedDict()
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)
return manager
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)
class AuthManager:
"""Manage the authentication for Home Assistant."""
def __init__(self, hass, store, providers):
"""Initialize the auth manager."""
self._store = store
self._providers = providers
self.login_flow = data_entry_flow.FlowManager(
hass, self._async_create_login_flow,
self._async_finish_login_flow)
self._access_tokens = {}
@property
def active(self):
"""Return if any auth providers are registered."""
return bool(self._providers)
@property
def support_legacy(self):
"""
Return if legacy_api_password auth providers are registered.
Should be removed when we removed legacy_api_password auth providers.
"""
for provider_type, _ in self._providers:
if provider_type == 'legacy_api_password':
return True
return False
@property
def async_auth_providers(self):
"""Return a list of available auth providers."""
return self._providers.values()
async def async_get_user(self, user_id):
"""Retrieve a user."""
return await self._store.async_get_user(user_id)
async def async_get_or_create_user(self, credentials):
"""Get or create a user."""
return await self._store.async_get_or_create_user(
credentials, self._async_get_auth_provider(credentials))
async def async_link_user(self, user, credentials):
"""Link credentials to an existing user."""
await self._store.async_link_user(user, credentials)
async def async_remove_user(self, user):
"""Remove a user."""
await self._store.async_remove_user(user)
async def async_create_refresh_token(self, user, client_id):
"""Create a new refresh token for a user."""
return await self._store.async_create_refresh_token(user, client_id)
async def async_get_refresh_token(self, token):
"""Get refresh token by token."""
return await self._store.async_get_refresh_token(token)
@callback
def async_create_access_token(self, refresh_token):
"""Create a new access token."""
access_token = AccessToken(refresh_token)
self._access_tokens[access_token.token] = access_token
return access_token
@callback
def async_get_access_token(self, token):
"""Get an access token."""
tkn = self._access_tokens.get(token)
if tkn is None:
return None
if tkn.expired:
self._access_tokens.pop(token)
return None
return tkn
async def async_create_client(self, name, *, redirect_uris=None,
no_secret=False):
"""Create a new client."""
return await self._store.async_create_client(
name, redirect_uris, no_secret)
async def async_get_or_create_client(self, name, *, redirect_uris=None,
no_secret=False):
"""Find a client, if not exists, create a new one."""
for client in await self._store.async_get_clients():
if client.name == name:
return client
return await self._store.async_create_client(
name, redirect_uris, no_secret)
async def async_get_client(self, client_id):
"""Get a client."""
return await self._store.async_get_client(client_id)
async def _async_create_login_flow(self, handler, *, source, data):
"""Create a login flow."""
auth_provider = self._providers[handler]
if not auth_provider.initialized:
auth_provider.initialized = True
await auth_provider.async_initialize()
return await auth_provider.async_credential_flow()
async def _async_finish_login_flow(self, result):
"""Result of a credential login flow."""
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return None
auth_provider = self._providers[result['handler']]
return await auth_provider.async_get_or_create_credentials(
result['data'])
@callback
def _async_get_auth_provider(self, credentials):
"""Helper to get auth provider from a set of credentials."""
auth_provider_key = (credentials.auth_provider_type,
credentials.auth_provider_id)
return self._providers[auth_provider_key]
class AuthStore:
"""Stores authentication info.
Any mutation to an object should happen inside the auth store.
The auth store is lazy. It won't load the data from disk until a method is
called that needs it.
"""
def __init__(self, hass):
"""Initialize the auth store."""
self.hass = hass
self._users = None
self._clients = None
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
async def credentials_for_provider(self, provider_type, provider_id):
"""Return credentials for specific auth provider type and id."""
if self._users is None:
await self.async_load()
return [
credentials
for user in self._users.values()
for credentials in user.credentials
if (credentials.auth_provider_type == provider_type and
credentials.auth_provider_id == provider_id)
]
async def async_get_users(self):
"""Retrieve all users."""
if self._users is None:
await self.async_load()
return list(self._users.values())
async def async_get_user(self, user_id):
"""Retrieve a user."""
if self._users is None:
await self.async_load()
return self._users.get(user_id)
async def async_get_or_create_user(self, credentials, auth_provider):
"""Get or create a new user for given credentials.
If link_user is passed in, the credentials will be linked to the passed
in user if the credentials are new.
"""
if self._users is None:
await self.async_load()
# New credentials, store in user
if credentials.is_new:
info = await auth_provider.async_user_meta_for_credentials(
credentials)
# Make owner and activate user if it's the first user.
if self._users:
is_owner = False
is_active = False
else:
is_owner = True
is_active = True
new_user = User(
is_owner=is_owner,
is_active=is_active,
name=info.get('name'),
)
self._users[new_user.id] = new_user
await self.async_link_user(new_user, credentials)
return new_user
for user in self._users.values():
for creds in user.credentials:
if (creds.auth_provider_type == credentials.auth_provider_type
and creds.auth_provider_id ==
credentials.auth_provider_id):
return user
raise ValueError('We got credentials with ID but found no user')
async def async_link_user(self, user, credentials):
"""Add credentials to an existing user."""
user.credentials.append(credentials)
await self.async_save()
credentials.is_new = False
async def async_remove_user(self, user):
"""Remove a user."""
self._users.pop(user.id)
await self.async_save()
async def async_create_refresh_token(self, user, client_id):
"""Create a new token for a user."""
local_user = await self.async_get_user(user.id)
if local_user is None:
raise ValueError('Invalid user')
local_client = await self.async_get_client(client_id)
if local_client is None:
raise ValueError('Invalid client_id')
refresh_token = RefreshToken(user, client_id)
user.refresh_tokens[refresh_token.token] = refresh_token
await self.async_save()
return refresh_token
async def async_get_refresh_token(self, token):
"""Get refresh token by token."""
if self._users is None:
await self.async_load()
for user in self._users.values():
refresh_token = user.refresh_tokens.get(token)
if refresh_token is not None:
return refresh_token
return None
async def async_create_client(self, name, redirect_uris, no_secret):
"""Create a new client."""
if self._clients is None:
await self.async_load()
kwargs = {
'name': name,
'redirect_uris': redirect_uris
}
if no_secret:
kwargs['secret'] = None
client = Client(**kwargs)
self._clients[client.id] = client
await self.async_save()
return client
async def async_get_clients(self):
"""Return all clients."""
if self._clients is None:
await self.async_load()
return list(self._clients.values())
async def async_get_client(self, client_id):
"""Get a client."""
if self._clients is None:
await self.async_load()
return self._clients.get(client_id)
async def async_load(self):
"""Load the users."""
data = await self._store.async_load()
# Make sure that we're not overriding data if 2 loads happened at the
# same time
if self._users is not None:
return
if data is None:
self._users = {}
self._clients = {}
return
users = {
user_dict['id']: User(**user_dict) for user_dict in data['users']
}
for cred_dict in data['credentials']:
users[cred_dict['user_id']].credentials.append(Credentials(
id=cred_dict['id'],
is_new=False,
auth_provider_type=cred_dict['auth_provider_type'],
auth_provider_id=cred_dict['auth_provider_id'],
data=cred_dict['data'],
))
refresh_tokens = {}
for rt_dict in data['refresh_tokens']:
token = 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']),
access_token_expiration=timedelta(
seconds=rt_dict['access_token_expiration']),
token=rt_dict['token'],
)
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 = 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)
clients = {
cl_dict['id']: Client(**cl_dict) for cl_dict in data['clients']
}
self._users = users
self._clients = clients
async def async_save(self):
"""Save users."""
users = [
{
'id': user.id,
'is_owner': user.is_owner,
'is_active': user.is_active,
'name': user.name,
}
for user in self._users.values()
]
credentials = [
{
'id': credential.id,
'user_id': user.id,
'auth_provider_type': credential.auth_provider_type,
'auth_provider_id': credential.auth_provider_id,
'data': credential.data,
}
for user in self._users.values()
for credential in user.credentials
]
refresh_tokens = [
{
'id': refresh_token.id,
'user_id': user.id,
'client_id': refresh_token.client_id,
'created_at': refresh_token.created_at.isoformat(),
'access_token_expiration':
refresh_token.access_token_expiration.total_seconds(),
'token': refresh_token.token,
}
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
]
clients = [
{
'id': client.id,
'name': client.name,
'secret': client.secret,
'redirect_uris': client.redirect_uris,
}
for client in self._clients.values()
]
data = {
'users': users,
'clients': clients,
'credentials': credentials,
'access_tokens': access_tokens,
'refresh_tokens': refresh_tokens,
}
await self._store.async_save(data, delay=1)

View File

@@ -0,0 +1,243 @@
"""Provide an authentication layer for Home Assistant."""
import asyncio
import logging
from collections import OrderedDict
from homeassistant import data_entry_flow
from homeassistant.core import callback
from . import models
from . import auth_store
from .providers import auth_provider_from_config
_LOGGER = logging.getLogger(__name__)
async def auth_manager_from_config(hass, provider_configs):
"""Initialize an auth manager from config."""
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 = []
# So returned auth providers are in same order as config
provider_hash = OrderedDict()
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)
return manager
class AuthManager:
"""Manage the authentication for Home Assistant."""
def __init__(self, hass, store, providers):
"""Initialize the auth manager."""
self._store = store
self._providers = providers
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):
"""Return if any auth providers are registered."""
return bool(self._providers)
@property
def support_legacy(self):
"""
Return if legacy_api_password auth providers are registered.
Should be removed when we removed legacy_api_password auth providers.
"""
for provider_type, _ in self._providers:
if provider_type == 'legacy_api_password':
return True
return False
@property
def auth_providers(self):
"""Return a list of available auth providers."""
return list(self._providers.values())
async def async_get_users(self):
"""Retrieve all users."""
return await self._store.async_get_users()
async def async_get_user(self, user_id):
"""Retrieve a user."""
return await self._store.async_get_user(user_id)
async def async_create_system_user(self, name):
"""Create a system user."""
return await self._store.async_create_user(
name=name,
system_generated=True,
is_active=True,
)
async def async_create_user(self, name):
"""Create a user."""
kwargs = {
'name': name,
'is_active': True,
}
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):
"""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.')
auth_provider = self._async_get_auth_provider(credentials)
if auth_provider is None:
raise RuntimeError('Credential with unknown provider encountered')
info = await auth_provider.async_user_meta_for_credentials(
credentials)
return await self._store.async_create_user(
credentials=credentials,
name=info.get('name'),
is_active=info.get('is_active', False)
)
async def async_link_user(self, user, credentials):
"""Link credentials to an existing user."""
await self._store.async_link_user(user, credentials)
async def async_remove_user(self, user):
"""Remove a user."""
tasks = [
self.async_remove_credentials(credentials)
for credentials in user.credentials
]
if tasks:
await asyncio.wait(tasks)
await self._store.async_remove_user(user)
async def async_activate_user(self, user):
"""Activate a user."""
await self._store.async_activate_user(user)
async def async_deactivate_user(self, user):
"""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):
"""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)
await self._store.async_remove_credentials(credentials)
async def async_create_refresh_token(self, user, client_id=None):
"""Create a new refresh token for a user."""
if not user.is_active:
raise ValueError('User is not active')
if user.system_generated and client_id is not None:
raise ValueError(
'System generated users cannot have refresh tokens connected '
'to a client.')
if not user.system_generated and client_id is None:
raise ValueError('Client is required to generate a refresh token.')
return await self._store.async_create_refresh_token(user, client_id)
async def async_get_refresh_token(self, token):
"""Get refresh token by token."""
return await self._store.async_get_refresh_token(token)
@callback
def async_create_access_token(self, refresh_token):
"""Create a new access token."""
access_token = models.AccessToken(refresh_token=refresh_token)
self._access_tokens[access_token.token] = access_token
return access_token
@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')
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)
return None
return tkn
async def _async_create_login_flow(self, handler, *, source, data):
"""Create a login flow."""
auth_provider = self._providers[handler]
return await auth_provider.async_credential_flow()
async def _async_finish_login_flow(self, result):
"""Result of a credential login flow."""
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return None
auth_provider = self._providers[result['handler']]
return await auth_provider.async_get_or_create_credentials(
result['data'])
@callback
def _async_get_auth_provider(self, credentials):
"""Helper to 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):
"""Determine if user should be owner.
A user should be an owner if it is the first non-system user that is
being created.
"""
for user in await self._store.async_get_users():
if not user.system_generated:
return False
return True

View File

@@ -0,0 +1,240 @@
"""Storage for auth models."""
from collections import OrderedDict
from datetime import timedelta
from homeassistant.util import dt as dt_util
from . import models
STORAGE_VERSION = 1
STORAGE_KEY = 'auth'
class AuthStore:
"""Stores authentication info.
Any mutation to an object should happen inside the auth store.
The auth store is lazy. It won't load the data from disk until a method is
called that needs it.
"""
def __init__(self, hass):
"""Initialize the auth store."""
self.hass = hass
self._users = None
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
async def async_get_users(self):
"""Retrieve all users."""
if self._users is None:
await self.async_load()
return list(self._users.values())
async def async_get_user(self, user_id):
"""Retrieve a user by id."""
if self._users is None:
await self.async_load()
return self._users.get(user_id)
async def async_create_user(self, name, is_owner=None, is_active=None,
system_generated=None, credentials=None):
"""Create a new user."""
if self._users is None:
await self.async_load()
kwargs = {
'name': name
}
if is_owner is not None:
kwargs['is_owner'] = is_owner
if is_active is not None:
kwargs['is_active'] = is_active
if system_generated is not None:
kwargs['system_generated'] = system_generated
new_user = models.User(**kwargs)
self._users[new_user.id] = new_user
if credentials is None:
await self.async_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):
"""Add credentials to an existing user."""
user.credentials.append(credentials)
await self.async_save()
credentials.is_new = False
async def async_remove_user(self, user):
"""Remove a user."""
self._users.pop(user.id)
await self.async_save()
async def async_activate_user(self, user):
"""Activate a user."""
user.is_active = True
await self.async_save()
async def async_deactivate_user(self, user):
"""Activate a user."""
user.is_active = False
await self.async_save()
async def async_remove_credentials(self, credentials):
"""Remove credentials."""
for user in self._users.values():
found = None
for index, cred in enumerate(user.credentials):
if cred is credentials:
found = index
break
if found is not None:
user.credentials.pop(found)
break
await self.async_save()
async def async_create_refresh_token(self, user, client_id=None):
"""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()
return refresh_token
async def async_get_refresh_token(self, token):
"""Get refresh token by token."""
if self._users is None:
await self.async_load()
for user in self._users.values():
refresh_token = user.refresh_tokens.get(token)
if refresh_token is not None:
return refresh_token
return None
async def async_load(self):
"""Load the users."""
data = await self._store.async_load()
# Make sure that we're not overriding data if 2 loads happened at the
# same time
if self._users is not None:
return
users = OrderedDict()
if data is None:
self._users = users
return
for user_dict in data['users']:
users[user_dict['id']] = models.User(**user_dict)
for cred_dict in data['credentials']:
users[cred_dict['user_id']].credentials.append(models.Credentials(
id=cred_dict['id'],
is_new=False,
auth_provider_type=cred_dict['auth_provider_type'],
auth_provider_id=cred_dict['auth_provider_id'],
data=cred_dict['data'],
))
refresh_tokens = OrderedDict()
for rt_dict in data['refresh_tokens']:
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']),
access_token_expiration=timedelta(
seconds=rt_dict['access_token_expiration']),
token=rt_dict['token'],
)
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)
self._users = users
async def async_save(self):
"""Save users."""
users = [
{
'id': user.id,
'is_owner': user.is_owner,
'is_active': user.is_active,
'name': user.name,
'system_generated': user.system_generated,
}
for user in self._users.values()
]
credentials = [
{
'id': credential.id,
'user_id': user.id,
'auth_provider_type': credential.auth_provider_type,
'auth_provider_id': credential.auth_provider_id,
'data': credential.data,
}
for user in self._users.values()
for credential in user.credentials
]
refresh_tokens = [
{
'id': refresh_token.id,
'user_id': user.id,
'client_id': refresh_token.client_id,
'created_at': refresh_token.created_at.isoformat(),
'access_token_expiration':
refresh_token.access_token_expiration.total_seconds(),
'token': refresh_token.token,
}
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 = {
'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,4 @@
"""Constants for the auth module."""
from datetime import timedelta
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)

View File

@@ -0,0 +1,75 @@
"""Auth models."""
from datetime import datetime, timedelta
import uuid
import attr
from homeassistant.util import dt as dt_util
from .const import ACCESS_TOKEN_EXPIRATION
from .util import generate_secret
@attr.s(slots=True)
class User:
"""A user."""
name = attr.ib(type=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)
# Tokens associated with a user.
refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict), cmp=False)
@attr.s(slots=True)
class RefreshToken:
"""RefreshToken for a user to grant new access tokens."""
user = attr.ib(type=User)
client_id = attr.ib(type=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
@attr.s(slots=True)
class Credentials:
"""Credentials for a user on an auth provider."""
auth_provider_type = attr.ib(type=str)
auth_provider_id = attr.ib(type=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)

View File

@@ -0,0 +1,143 @@
"""Auth providers for Home Assistant."""
import importlib
import logging
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.util.decorator import Registry
from homeassistant.auth.models import Credentials
_LOGGER = logging.getLogger(__name__)
DATA_REQS = 'auth_prov_reqs_processed'
AUTH_PROVIDERS = Registry()
AUTH_PROVIDER_SCHEMA = vol.Schema({
vol.Required(CONF_TYPE): str,
vol.Optional(CONF_NAME): str,
# Specify ID if you have two auth providers for same type.
vol.Optional(CONF_ID): str,
}, 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):
"""Initialize an auth provider."""
self.hass = hass
self.store = store
self.config = config
@property
def id(self): # pylint: disable=invalid-name
"""Return id of the auth provider.
Optional, can be None.
"""
return self.config.get(CONF_ID)
@property
def type(self):
"""Return type of the provider."""
return self.config[CONF_TYPE]
@property
def name(self):
"""Return the name of the auth provider."""
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
async def async_credentials(self):
"""Return all credentials of this provider."""
users = await self.store.async_get_users()
return [
credentials
for user in users
for credentials in user.credentials
if (credentials.auth_provider_type == self.type and
credentials.auth_provider_id == self.id)
]
@callback
def async_create_credentials(self, data):
"""Create credentials."""
return Credentials(
auth_provider_type=self.type,
auth_provider_id=self.id,
data=data,
)
# Implement by extending class
async def async_credential_flow(self):
"""Return the data flow for logging in with auth provider."""
raise NotImplementedError
async def async_get_or_create_credentials(self, flow_result):
"""Get credentials based on the flow result."""
raise NotImplementedError
async def async_user_meta_for_credentials(self, credentials):
"""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 {}

View File

@@ -6,15 +6,29 @@ import hmac
import voluptuous as vol
from homeassistant import auth, data_entry_flow
from homeassistant import data_entry_flow
from homeassistant.const import CONF_ID
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.auth.util import generate_secret
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
STORAGE_VERSION = 1
STORAGE_KEY = 'auth_provider.homeassistant'
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA)
def _disallow_id(conf):
"""Disallow ID in config."""
if CONF_ID in conf:
raise vol.Invalid(
'ID is not allowed for the homeassistant auth provider.')
return conf
CONFIG_SCHEMA = vol.All(AUTH_PROVIDER_SCHEMA, _disallow_id)
class InvalidAuth(HomeAssistantError):
@@ -43,7 +57,7 @@ class Data:
if data is None:
data = {
'salt': auth.generate_secret(),
'salt': generate_secret(),
'users': []
}
@@ -85,8 +99,8 @@ class Data:
hashed = base64.b64encode(hashed).decode()
return hashed
def add_user(self, username, password):
"""Add a user."""
def add_auth(self, username, password):
"""Add a new authenticated user/pass."""
if any(user['username'] == username for user in self.users):
raise InvalidUser
@@ -95,8 +109,22 @@ class Data:
'password': self.hash_password(password, True),
})
@callback
def async_remove_auth(self, username):
"""Remove authentication."""
index = None
for i, user in enumerate(self.users):
if user['username'] == username:
index = i
break
if index is None:
raise InvalidUser
self.users.pop(index)
def change_password(self, username, new_password):
"""Update the password of a user.
"""Update the password.
Raises InvalidUser if user cannot be found.
"""
@@ -112,22 +140,33 @@ class Data:
await self._store.async_save(self._data)
@auth.AUTH_PROVIDERS.register('homeassistant')
class HassAuthProvider(auth.AuthProvider):
@AUTH_PROVIDERS.register('homeassistant')
class HassAuthProvider(AuthProvider):
"""Auth provider based on a local storage of users in HASS config dir."""
DEFAULT_TITLE = 'Home Assistant Local'
data = None
async def async_initialize(self):
"""Initialize the auth provider."""
if self.data is not None:
return
self.data = Data(self.hass)
await self.data.async_load()
async def async_credential_flow(self):
"""Return a flow to login."""
return LoginFlow(self)
async def async_validate_login(self, username, password):
"""Helper to validate a username and password."""
data = Data(self.hass)
await data.async_load()
if self.data is None:
await self.async_initialize()
await self.hass.async_add_executor_job(
data.validate_login, username, password)
self.data.validate_login, username, password)
async def async_get_or_create_credentials(self, flow_result):
"""Get credentials based on the flow result."""
@@ -142,6 +181,25 @@ class HassAuthProvider(auth.AuthProvider):
'username': username
})
async def async_user_meta_for_credentials(self, credentials):
"""Get extra info for this credential."""
return {
'name': credentials.data['username'],
'is_active': True,
}
async def async_will_remove_credentials(self, credentials):
"""When credentials get removed, also remove the auth."""
if self.data is None:
await self.async_initialize()
try:
self.data.async_remove_auth(credentials.data['username'])
await self.data.async_save()
except InvalidUser:
# Can happen if somehow we didn't clean up a credential
pass
class LoginFlow(data_entry_flow.FlowHandler):
"""Handler for the login flow."""

View File

@@ -5,9 +5,11 @@ import hmac
import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError
from homeassistant import auth, data_entry_flow
from homeassistant import data_entry_flow
from homeassistant.core import callback
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
USER_SCHEMA = vol.Schema({
vol.Required('username'): str,
@@ -16,7 +18,7 @@ USER_SCHEMA = vol.Schema({
})
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
vol.Required('users'): [USER_SCHEMA]
}, extra=vol.PREVENT_EXTRA)
@@ -25,8 +27,8 @@ class InvalidAuthError(HomeAssistantError):
"""Raised when submitting invalid authentication."""
@auth.AUTH_PROVIDERS.register('insecure_example')
class ExampleAuthProvider(auth.AuthProvider):
@AUTH_PROVIDERS.register('insecure_example')
class ExampleAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords."""
async def async_credential_flow(self):
@@ -73,14 +75,16 @@ class ExampleAuthProvider(auth.AuthProvider):
Will be used to populate info when creating a new user.
"""
username = credentials.data['username']
info = {
'is_active': True,
}
for user in self.config['users']:
if user['username'] == username:
return {
'name': user.get('name')
}
info['name'] = user.get('name')
break
return {}
return info
class LoginFlow(data_entry_flow.FlowHandler):

View File

@@ -9,15 +9,18 @@ import hmac
import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError
from homeassistant import auth, data_entry_flow
from homeassistant import data_entry_flow
from homeassistant.core import callback
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
USER_SCHEMA = vol.Schema({
vol.Required('username'): str,
})
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA)
LEGACY_USER = 'homeassistant'
@@ -27,8 +30,8 @@ class InvalidAuthError(HomeAssistantError):
"""Raised when submitting invalid authentication."""
@auth.AUTH_PROVIDERS.register('legacy_api_password')
class LegacyApiPasswordAuthProvider(auth.AuthProvider):
@AUTH_PROVIDERS.register('legacy_api_password')
class LegacyApiPasswordAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords."""
DEFAULT_TITLE = 'Legacy API Password'
@@ -67,7 +70,10 @@ class LegacyApiPasswordAuthProvider(auth.AuthProvider):
Will be used to populate info when creating a new user.
"""
return {'name': LEGACY_USER}
return {
'name': LEGACY_USER,
'is_active': True,
}
class LoginFlow(data_entry_flow.FlowHandler):

View File

@@ -0,0 +1,13 @@
"""Auth utils."""
import binascii
import os
def generate_secret(entropy: int = 32) -> str:
"""Generate a secret.
Backport of secrets.token_hex from Python 3.6
Event loop friendly.
"""
return binascii.hexlify(os.urandom(entropy)).decode('ascii')

View File

@@ -1 +0,0 @@
"""Auth providers for Home Assistant."""

View File

@@ -28,9 +28,8 @@ ERROR_LOG_FILENAME = 'home-assistant.log'
# hass.data key for logging information.
DATA_LOGGING = 'logging'
FIRST_INIT_COMPONENT = set((
'system_log', 'recorder', 'mqtt', 'mqtt_eventstream', 'logger',
'introduction', 'frontend', 'history'))
FIRST_INIT_COMPONENT = {'system_log', 'recorder', 'mqtt', 'mqtt_eventstream',
'logger', 'introduction', 'frontend', 'history'}
def from_config_dict(config: Dict[str, Any],
@@ -95,7 +94,8 @@ async def async_from_config_dict(config: Dict[str, Any],
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
return None
await hass.async_add_job(conf_util.process_ha_config_upgrade, hass)
await hass.async_add_executor_job(
conf_util.process_ha_config_upgrade, hass)
hass.config.skip_pip = skip_pip
if skip_pip:
@@ -137,7 +137,7 @@ async def async_from_config_dict(config: Dict[str, Any],
for component in components:
if component not in FIRST_INIT_COMPONENT:
continue
hass.async_add_job(async_setup_component(hass, component, config))
hass.async_create_task(async_setup_component(hass, component, config))
await hass.async_block_till_done()
@@ -145,7 +145,7 @@ async def async_from_config_dict(config: Dict[str, Any],
for component in components:
if component in FIRST_INIT_COMPONENT:
continue
hass.async_add_job(async_setup_component(hass, component, config))
hass.async_create_task(async_setup_component(hass, component, config))
await hass.async_block_till_done()
@@ -162,7 +162,8 @@ def from_config_file(config_path: str,
skip_pip: bool = True,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False):
log_no_color: bool = False)\
-> Optional[core.HomeAssistant]:
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter if given,
@@ -187,7 +188,8 @@ async def async_from_config_file(config_path: str,
skip_pip: bool = True,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False):
log_no_color: bool = False)\
-> Optional[core.HomeAssistant]:
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter.
@@ -204,7 +206,7 @@ async def async_from_config_file(config_path: str,
log_no_color)
try:
config_dict = await hass.async_add_job(
config_dict = await hass.async_add_executor_job(
conf_util.load_yaml_config_file, config_path)
except HomeAssistantError as err:
_LOGGER.error("Error loading %s: %s", config_path, err)

View File

@@ -121,7 +121,7 @@ def alarm_arm_custom_bypass(hass, code=None, entity_id=None):
@asyncio.coroutine
def async_setup(hass, config):
"""Track states and offer events for sensors."""
component = EntityComponent(
component = hass.data[DOMAIN] = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
yield from component.async_setup(config)
@@ -154,6 +154,17 @@ def async_setup(hass, config):
return True
async def async_setup_entry(hass, entry):
"""Setup a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)
# pylint: disable=no-self-use
class AlarmControlPanel(Entity):
"""An abstract class for alarm control devices."""

View File

@@ -0,0 +1,88 @@
"""
Support for HomematicIP alarm control panel.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.homematicip_cloud/
"""
import logging
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__)
HMIP_OPEN = 'OPEN'
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."""
pass
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up the HomematicIP alarm control panel from a config entry."""
from homematicip.aio.group import AsyncSecurityZoneGroup
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
devices = []
for group in home.groups:
if isinstance(group, AsyncSecurityZoneGroup):
devices.append(HomematicipSecurityZone(home, group))
if devices:
async_add_devices(devices)
class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
"""Representation of an HomematicIP security zone group."""
def __init__(self, home, device):
"""Initialize the security zone group."""
device.modelType = 'Group-SecurityZone'
device.windowState = ''
super().__init__(home, device)
@property
def state(self):
"""Return the state of the device."""
if self._device.active:
if (self._device.sabotage or self._device.motionDetected or
self._device.windowState == HMIP_OPEN):
return STATE_ALARM_TRIGGERED
if self._device.label == HMIP_ZONE_HOME:
return STATE_ALARM_ARMED_HOME
return STATE_ALARM_ARMED_AWAY
return STATE_ALARM_DISARMED
async def async_alarm_disarm(self, code=None):
"""Send disarm command."""
await self._home.set_security_zones_activation(False, False)
async def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
await self._home.set_security_zones_activation(True, False)
async def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
await self._home.set_security_zones_activation(True, True)
@property
def device_state_attributes(self):
"""Return the state attributes of the alarm control device."""
# The base class is loading the battery property, but device doesn't
# have this property - base class needs clean-up.
return None

View File

@@ -270,11 +270,14 @@ class _AlexaInterface(object):
"""Return properties serialized for an API response."""
for prop in self.properties_supported():
prop_name = prop['name']
yield {
'name': prop_name,
'namespace': self.name(),
'value': self.get_property(prop_name),
}
# pylint: disable=assignment-from-no-return
prop_value = self.get_property(prop_name)
if prop_value is not None:
yield {
'name': prop_name,
'namespace': self.name(),
'value': prop_value,
}
class _AlexaPowerController(_AlexaInterface):
@@ -438,14 +441,17 @@ class _AlexaThermostatController(_AlexaInterface):
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
temp = None
if name == 'targetSetpoint':
temp = self.entity.attributes.get(ATTR_TEMPERATURE)
temp = self.entity.attributes.get(climate.ATTR_TEMPERATURE)
elif name == 'lowerSetpoint':
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
elif name == 'upperSetpoint':
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
if temp is None:
else:
raise _UnsupportedProperty(name)
if temp is None:
return None
return {
'value': float(temp),
'scale': API_TEMP_UNITS[unit],

View File

@@ -16,7 +16,7 @@ from homeassistant.const import (
from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.dispatcher import dispatcher_send
REQUIREMENTS = ['pyarlo==0.1.8']
REQUIREMENTS = ['pyarlo==0.1.9']
_LOGGER = logging.getLogger(__name__)

View File

@@ -102,6 +102,7 @@ a limited expiration.
"token_type": "Bearer"
}
"""
from datetime import timedelta
import logging
import uuid
@@ -112,13 +113,22 @@ from homeassistant import data_entry_flow
from homeassistant.core import callback
from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView, FlowManagerResourceView)
from homeassistant.components import websocket_api
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.util import dt as dt_util
from . import indieauth
from .client import verify_client
DOMAIN = 'auth'
DEPENDENCIES = ['http']
WS_TYPE_CURRENT_USER = 'auth/current_user'
SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_CURRENT_USER,
})
_LOGGER = logging.getLogger(__name__)
@@ -133,6 +143,11 @@ async def async_setup(hass, config):
hass.http.register_view(GrantTokenView(retrieve_credentials))
hass.http.register_view(LinkUserView(retrieve_credentials))
hass.components.websocket_api.async_register_command(
WS_TYPE_CURRENT_USER, websocket_current_user,
SCHEMA_WS_CURRENT_USER
)
return True
@@ -143,14 +158,13 @@ class AuthProvidersView(HomeAssistantView):
name = 'api:auth:providers'
requires_auth = False
@verify_client
async def get(self, request, client):
async def get(self, request):
"""Get available auth providers."""
return self.json([{
'name': provider.name,
'id': provider.id,
'type': provider.type,
} for provider in request.app['hass'].auth.async_auth_providers])
} for provider in request.app['hass'].auth.auth_providers])
class LoginFlowIndexView(FlowManagerIndexView):
@@ -164,16 +178,16 @@ class LoginFlowIndexView(FlowManagerIndexView):
"""Do not allow index of flows in progress."""
return aiohttp.web.Response(status=405)
# pylint: disable=arguments-differ
@verify_client
@RequestDataValidator(vol.Schema({
vol.Required('client_id'): str,
vol.Required('handler'): vol.Any(str, list),
vol.Required('redirect_uri'): str,
}))
async def post(self, request, client, data):
async def post(self, request, data):
"""Create a new login flow."""
if data['redirect_uri'] not in client.redirect_uris:
return self.json_message('invalid redirect uri', )
if not indieauth.verify_redirect_uri(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)
@@ -191,16 +205,20 @@ class LoginFlowResourceView(FlowManagerResourceView):
super().__init__(flow_mgr)
self._store_credentials = store_credentials
# pylint: disable=arguments-differ
async def get(self, request):
async def get(self, request, flow_id):
"""Do not allow getting status of a flow in progress."""
return self.json_message('Invalid flow specified', 404)
# pylint: disable=arguments-differ
@verify_client
@RequestDataValidator(vol.Schema(dict), allow_empty=True)
async def post(self, request, client, flow_id, data):
@RequestDataValidator(vol.Schema({
'client_id': str
}, extra=vol.ALLOW_EXTRA))
async def post(self, request, flow_id, data):
"""Handle progressing a login flow request."""
client_id = data.pop('client_id')
if not indieauth.verify_client_id(client_id):
return self.json_message('Invalid client id', 400)
try:
result = await self._flow_mgr.async_configure(flow_id, data)
except data_entry_flow.UnknownFlow:
@@ -212,7 +230,7 @@ class LoginFlowResourceView(FlowManagerResourceView):
return self.json(self._prepare_result_json(result))
result.pop('data')
result['result'] = self._store_credentials(client.id, result['result'])
result['result'] = self._store_credentials(client_id, result['result'])
return self.json(result)
@@ -223,25 +241,32 @@ class GrantTokenView(HomeAssistantView):
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
@verify_client
async def post(self, request, client):
async def post(self, request):
"""Grant a token."""
hass = request.app['hass']
data = await request.post()
client_id = data.get('client_id')
if client_id is None or not indieauth.verify_client_id(client_id):
return self.json({
'error': 'invalid_request',
'error_description': 'Invalid client id',
}, status_code=400)
grant_type = data.get('grant_type')
if grant_type == 'authorization_code':
return await self._async_handle_auth_code(
hass, client.id, data)
return await self._async_handle_auth_code(hass, client_id, data)
elif grant_type == 'refresh_token':
return await self._async_handle_refresh_token(
hass, client.id, data)
hass, client_id, data)
return self.json({
'error': 'unsupported_grant_type',
@@ -261,9 +286,17 @@ class GrantTokenView(HomeAssistantView):
if credentials is None:
return self.json({
'error': 'invalid_request',
'error_description': 'Invalid code',
}, status_code=400)
user = await hass.auth.async_get_or_create_user(credentials)
if not user.is_active:
return self.json({
'error': 'access_denied',
'error_description': 'User is not active',
}, status_code=403)
refresh_token = await hass.auth.async_create_refresh_token(user,
client_id)
access_token = hass.auth.async_create_access_token(refresh_token)
@@ -340,12 +373,43 @@ def _create_cred_store():
def store_credentials(client_id, credentials):
"""Store credentials and return a code to retrieve it."""
code = uuid.uuid4().hex
temp_credentials[(client_id, code)] = credentials
temp_credentials[(client_id, code)] = (dt_util.utcnow(), credentials)
return code
@callback
def retrieve_credentials(client_id, code):
"""Retrieve credentials."""
return temp_credentials.pop((client_id, code), None)
key = (client_id, code)
if key not in temp_credentials:
return None
created, credentials = temp_credentials.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 None
return store_credentials, retrieve_credentials
@callback
def websocket_current_user(hass, connection, msg):
"""Return the current user."""
user = connection.request.get('hass_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.to_write.put_nowait(websocket_api.result_message(msg['id'], {
'id': user.id,
'name': user.name,
'is_owner': user.is_owner,
}))

View File

@@ -1,79 +0,0 @@
"""Helpers to resolve client ID/secret."""
import base64
from functools import wraps
import hmac
import aiohttp.hdrs
def verify_client(method):
"""Decorator to verify client id/secret on requests."""
@wraps(method)
async def wrapper(view, request, *args, **kwargs):
"""Verify client id/secret before doing request."""
client = await _verify_client(request)
if client is None:
return view.json({
'error': 'invalid_client',
}, status_code=401)
return await method(
view, request, *args, **kwargs, client=client)
return wrapper
async def _verify_client(request):
"""Method to verify the client id/secret in consistent time.
By using a consistent time for looking up client id and comparing the
secret, we prevent attacks by malicious actors trying different client ids
and are able to derive from the time it takes to process the request if
they guessed the client id correctly.
"""
if aiohttp.hdrs.AUTHORIZATION not in request.headers:
return None
auth_type, auth_value = \
request.headers.get(aiohttp.hdrs.AUTHORIZATION).split(' ', 1)
if auth_type != 'Basic':
return None
decoded = base64.b64decode(auth_value).decode('utf-8')
try:
client_id, client_secret = decoded.split(':', 1)
except ValueError:
# If no ':' in decoded
client_id, client_secret = decoded, None
return await async_secure_get_client(
request.app['hass'], client_id, client_secret)
async def async_secure_get_client(hass, client_id, client_secret):
"""Get a client id/secret in consistent time."""
client = await hass.auth.async_get_client(client_id)
if client is None:
if client_secret is not None:
# Still do a compare so we run same time as if a client was found.
hmac.compare_digest(client_secret.encode('utf-8'),
client_secret.encode('utf-8'))
return None
if client.secret is None:
return client
elif client_secret is None:
# Still do a compare so we run same time as if a secret was passed.
hmac.compare_digest(client.secret.encode('utf-8'),
client.secret.encode('utf-8'))
return None
elif hmac.compare_digest(client_secret.encode('utf-8'),
client.secret.encode('utf-8')):
return client
return None

View File

@@ -0,0 +1,130 @@
"""Helpers to resolve client ID/secret."""
from ipaddress import ip_address, ip_network
from urllib.parse import urlparse
# IP addresses of loopback interfaces
ALLOWED_IPS = (
ip_address('127.0.0.1'),
ip_address('::1'),
)
# RFC1918 - Address allocation for Private Internets
ALLOWED_NETWORKS = (
ip_network('10.0.0.0/8'),
ip_network('172.16.0.0/12'),
ip_network('192.168.0.0/16'),
)
def verify_redirect_uri(client_id, redirect_uri):
"""Verify that the client and redirect uri match."""
try:
client_id_parts = _parse_client_id(client_id)
except ValueError:
return False
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 (
client_id_parts.scheme == redirect_parts.scheme and
client_id_parts.netloc == redirect_parts.netloc
)
def verify_client_id(client_id):
"""Verify that the client id is valid."""
try:
_parse_client_id(client_id)
return True
except ValueError:
return False
def _parse_url(url):
"""Parse a url in parts and canonicalize according to IndieAuth."""
parts = urlparse(url)
# Canonicalize a url according to IndieAuth 3.2.
# SHOULD convert the hostname to lowercase
parts = parts._replace(netloc=parts.netloc.lower())
# If a URL with no path component is ever encountered,
# it MUST be treated as if it had the path /.
if parts.path == '':
parts = parts._replace(path='/')
return parts
def _parse_client_id(client_id):
"""Test if client id is a valid URL according to IndieAuth section 3.2.
https://indieauth.spec.indieweb.org/#client-identifier
"""
parts = _parse_url(client_id)
# Client identifier URLs
# MUST have either an https or http scheme
if parts.scheme not in ('http', 'https'):
raise ValueError()
# MUST contain a path component
# Handled by url canonicalization.
# MUST NOT contain single-dot or double-dot path segments
if any(segment in ('.', '..') for segment in parts.path.split('/')):
raise ValueError(
'Client ID cannot contain single-dot or double-dot path segments')
# MUST NOT contain a fragment component
if parts.fragment != '':
raise ValueError('Client ID cannot contain a fragment')
# MUST NOT contain a username or password component
if parts.username is not None:
raise ValueError('Client ID cannot contain username')
if parts.password is not None:
raise ValueError('Client ID cannot contain password')
# MAY contain a port
try:
# parts raises ValueError when port cannot be parsed as int
parts.port
except ValueError:
raise ValueError('Client ID contains invalid port')
# Additionally, hostnames
# MUST be domain names or a loopback interface and
# MUST NOT be IPv4 or IPv6 addresses except for IPv4 127.0.0.1
# or IPv6 [::1]
# We are not goint to follow the spec here. We are going to allow
# any internal network IP to be used inside a client id.
address = None
try:
netloc = parts.netloc
# Strip the [, ] from ipv6 addresses before parsing
if netloc[0] == '[' and netloc[-1] == ']':
netloc = netloc[1:-1]
address = ip_address(netloc)
except ValueError:
# Not an ip address
pass
if (address is None or
address in ALLOWED_IPS or
any(address in network for network in ALLOWED_NETWORKS)):
return parts
raise ValueError('Hostname should be a domain name or local IP address')

View File

@@ -5,9 +5,9 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.deconz/
"""
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz import (
CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID,
DATA_DECONZ_UNSUB)
from homeassistant.components.deconz.const import (
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB)
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -62,7 +62,8 @@ class DeconzBinarySensor(BinarySensorDevice):
"""
if reason['state'] or \
'reachable' in reason['attr'] or \
'battery' in reason['attr']:
'battery' in reason['attr'] or \
'on' in reason['attr']:
self.async_schedule_update_ha_state()
@property
@@ -107,6 +108,8 @@ class DeconzBinarySensor(BinarySensorDevice):
attr = {}
if self._sensor.battery:
attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
if self._sensor.on is not None:
attr[ATTR_ON] = self._sensor.on
if self._sensor.type in PRESENCE and self._sensor.dark is not None:
attr['dark'] = self._sensor.dark
attr[ATTR_DARK] = self._sensor.dark
return attr

View File

@@ -9,8 +9,8 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.homematicip_cloud import (
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
ATTR_HOME_ID)
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
HMIPC_HAPID)
DEPENDENCIES = ['homematicip_cloud']
@@ -21,17 +21,18 @@ ATTR_EVENT_DELAY = 'event_delay'
ATTR_MOTION_DETECTED = 'motion_detected'
ATTR_ILLUMINATION = 'illumination'
HMIP_OPEN = 'open'
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the HomematicIP binary sensor devices."""
"""Set up the 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)
if discovery_info is None:
return
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
devices = []
for device in home.devices:
if isinstance(device, ShutterContact):
@@ -58,11 +59,13 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
@property
def is_on(self):
"""Return true if the shutter contact is on/open."""
from homematicip.base.enums import WindowState
if self._device.sabotage:
return True
if self._device.windowState is None:
return None
return self._device.windowState.lower() == HMIP_OPEN
return self._device.windowState == WindowState.OPEN
class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):

View File

@@ -23,7 +23,7 @@ DEPENDENCIES = ['ring']
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5)
SCAN_INTERVAL = timedelta(seconds=10)
# Sensor types: Name, category, device_class
SENSOR_TYPES = {

View File

@@ -41,8 +41,9 @@ async def async_setup(hass, config):
hass.http.register_view(CalendarListView(component))
hass.http.register_view(CalendarEventView(component))
await hass.components.frontend.async_register_built_in_panel(
'calendar', 'calendar', 'hass:calendar')
# Doesn't work in prod builds of the frontend: home-assistant-polymer#1289
# await hass.components.frontend.async_register_built_in_panel(
# 'calendar', 'calendar', 'hass:calendar')
await component.async_setup(config)
return True

View File

@@ -4,7 +4,6 @@ Support for Google Calendar Search binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.google_calendar/
"""
# pylint: disable=import-error
import logging
from datetime import timedelta

View File

@@ -66,8 +66,8 @@ CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({
WS_TYPE_CAMERA_THUMBNAIL = 'camera_thumbnail'
SCHEMA_WS_CAMERA_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
'type': WS_TYPE_CAMERA_THUMBNAIL,
'entity_id': cv.entity_id
vol.Required('type'): WS_TYPE_CAMERA_THUMBNAIL,
vol.Required('entity_id'): cv.entity_id
})

View File

@@ -25,9 +25,7 @@ _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['onvif-py3==0.1.3',
'suds-py3==1.3.3.0',
'http://github.com/tgaugry/suds-passworddigest-py3'
'/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip'
'#suds-passworddigest-py3==0.1.2a']
'suds-passworddigest-homeassistant==0.1.2a0.dev0']
DEPENDENCIES = ['ffmpeg']
DEFAULT_NAME = 'ONVIF Camera'
DEFAULT_PORT = 5000

View File

@@ -0,0 +1,170 @@
"""
Camera platform that receives images through HTTP POST.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/camera.push/
"""
import logging
from collections import deque
from datetime import timedelta
import voluptuous as vol
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\
STATE_IDLE, STATE_RECORDING
from homeassistant.core import callback
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.const import CONF_NAME, CONF_TIMEOUT, HTTP_BAD_REQUEST
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import async_track_point_in_utc_time
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
CONF_BUFFER_SIZE = 'buffer'
CONF_IMAGE_FIELD = 'field'
DEFAULT_NAME = "Push Camera"
ATTR_FILENAME = 'filename'
ATTR_LAST_TRIP = 'last_trip'
PUSH_CAMERA_DATA = 'push_camera'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_BUFFER_SIZE, default=1): cv.positive_int,
vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All(
cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_IMAGE_FIELD, default='image'): cv.string,
})
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the Push Camera platform."""
if PUSH_CAMERA_DATA not in hass.data:
hass.data[PUSH_CAMERA_DATA] = {}
cameras = [PushCamera(config[CONF_NAME],
config[CONF_BUFFER_SIZE],
config[CONF_TIMEOUT])]
hass.http.register_view(CameraPushReceiver(hass,
config[CONF_IMAGE_FIELD]))
async_add_devices(cameras)
class CameraPushReceiver(HomeAssistantView):
"""Handle pushes from remote camera."""
url = "/api/camera_push/{entity_id}"
name = 'api:camera_push:camera_entity'
def __init__(self, hass, image_field):
"""Initialize CameraPushReceiver with camera entity."""
self._cameras = hass.data[PUSH_CAMERA_DATA]
self._image = image_field
async def post(self, request, entity_id):
"""Accept the POST from Camera."""
_camera = self._cameras.get(entity_id)
if _camera is None:
_LOGGER.error("Unknown %s", entity_id)
return self.json_message('Unknown {}'.format(entity_id),
HTTP_BAD_REQUEST)
try:
data = await request.post()
_LOGGER.debug("Received Camera push: %s", data[self._image])
await _camera.update_image(data[self._image].file.read(),
data[self._image].filename)
except ValueError as value_error:
_LOGGER.error("Unknown value %s", value_error)
return self.json_message('Invalid POST', HTTP_BAD_REQUEST)
except KeyError as key_error:
_LOGGER.error('In your POST message %s', key_error)
return self.json_message('{} missing'.format(self._image),
HTTP_BAD_REQUEST)
class PushCamera(Camera):
"""The representation of a Push camera."""
def __init__(self, name, buffer_size, timeout):
"""Initialize push camera component."""
super().__init__()
self._name = name
self._last_trip = None
self._filename = None
self._expired_listener = None
self._state = STATE_IDLE
self._timeout = timeout
self.queue = deque([], buffer_size)
self._current_image = None
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
self.hass.data[PUSH_CAMERA_DATA][self.entity_id] = self
@property
def state(self):
"""Current state of the camera."""
return self._state
async def update_image(self, image, filename):
"""Update the camera image."""
if self._state == STATE_IDLE:
self._state = STATE_RECORDING
self._last_trip = dt_util.utcnow()
self.queue.clear()
self._filename = filename
self.queue.appendleft(image)
@callback
def reset_state(now):
"""Set state to idle after no new images for a period of time."""
self._state = STATE_IDLE
self._expired_listener = None
_LOGGER.debug("Reset state")
self.async_schedule_update_ha_state()
if self._expired_listener:
self._expired_listener()
self._expired_listener = async_track_point_in_utc_time(
self.hass, reset_state, dt_util.utcnow() + self._timeout)
self.async_schedule_update_ha_state()
async def async_camera_image(self):
"""Return a still image response."""
if self.queue:
if self._state == STATE_IDLE:
self.queue.rotate(1)
self._current_image = self.queue[0]
return self._current_image
@property
def name(self):
"""Return the name of this camera."""
return self._name
@property
def motion_detection_enabled(self):
"""Camera Motion Detection Status."""
return False
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
name: value for name, value in (
(ATTR_LAST_TRIP, self._last_trip),
(ATTR_FILENAME, self._filename),
) if value is not None
}

0
homeassistant/components/climate/fritzbox.py Executable file → Normal file
View File

View File

@@ -12,8 +12,8 @@ from homeassistant.components.climate import (
STATE_AUTO, STATE_MANUAL)
from homeassistant.const import TEMP_CELSIUS
from homeassistant.components.homematicip_cloud import (
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
ATTR_HOME_ID)
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
HMIPC_HAPID)
_LOGGER = logging.getLogger(__name__)
@@ -30,12 +30,14 @@ HMIP_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_HMIP.items()}
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the HomematicIP climate devices."""
pass
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up the HomematicIP climate from a config entry."""
from homematicip.group import HeatingGroup
if discovery_info is None:
return
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
devices = []
for device in home.groups:
if isinstance(device, HeatingGroup):

View File

@@ -0,0 +1,77 @@
"""
Update the IP addresses of your Cloudflare DNS records.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/cloudflare/
"""
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.const import CONF_API_KEY, CONF_EMAIL, CONF_ZONE
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_time_interval
REQUIREMENTS = ['pycfdns==0.0.1']
_LOGGER = logging.getLogger(__name__)
CONF_RECORDS = 'records'
DOMAIN = 'cloudflare'
INTERVAL = timedelta(minutes=60)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_EMAIL): cv.string,
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_ZONE): cv.string,
vol.Required(CONF_RECORDS): vol.All(cv.ensure_list, [cv.string]),
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the Cloudflare component."""
from pycfdns import CloudflareUpdater
cfupdate = CloudflareUpdater()
email = config[DOMAIN][CONF_EMAIL]
key = config[DOMAIN][CONF_API_KEY]
zone = config[DOMAIN][CONF_ZONE]
records = config[DOMAIN][CONF_RECORDS]
def update_records_interval(now):
"""Set up recurring update."""
_update_cloudflare(cfupdate, email, key, zone, records)
def update_records_service(now):
"""Set up service for manual trigger."""
_update_cloudflare(cfupdate, email, key, zone, records)
track_time_interval(hass, update_records_interval, INTERVAL)
hass.services.register(
DOMAIN, 'update_records', update_records_service)
return True
def _update_cloudflare(cfupdate, email, key, zone, records):
"""Update DNS records for a given zone."""
_LOGGER.debug("Starting update for zone %s", zone)
headers = cfupdate.set_header(email, key)
_LOGGER.debug("Header data defined as: %s", headers)
zoneid = cfupdate.get_zoneID(headers, zone)
_LOGGER.debug("Zone ID is set to: %s", zoneid)
update_records = cfupdate.get_recordInfo(headers, zoneid, zone, records)
_LOGGER.debug("Records: %s", update_records)
result = cfupdate.update_records(headers, zoneid, update_records)
_LOGGER.debug("Update for zone %s is complete", zone)
if result is not True:
_LOGGER.warning(result)

View File

@@ -49,6 +49,10 @@ async def async_setup(hass, config):
tasks = [setup_panel(panel_name) for panel_name in SECTIONS]
if hass.auth.active:
tasks.append(setup_panel('auth'))
tasks.append(setup_panel('auth_provider_homeassistant'))
for panel_name in ON_DEMAND:
if panel_name in hass.config.components:
tasks.append(setup_panel(panel_name))

View File

@@ -0,0 +1,113 @@
"""Offer API to configure Home Assistant auth."""
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components import websocket_api
WS_TYPE_LIST = 'config/auth/list'
SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_LIST,
})
WS_TYPE_DELETE = 'config/auth/delete'
SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_DELETE,
vol.Required('user_id'): str,
})
WS_TYPE_CREATE = 'config/auth/create'
SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_CREATE,
vol.Required('name'): str,
})
async def async_setup(hass):
"""Enable the Home Assistant views."""
hass.components.websocket_api.async_register_command(
WS_TYPE_LIST, websocket_list,
SCHEMA_WS_LIST
)
hass.components.websocket_api.async_register_command(
WS_TYPE_DELETE, websocket_delete,
SCHEMA_WS_DELETE
)
hass.components.websocket_api.async_register_command(
WS_TYPE_CREATE, websocket_create,
SCHEMA_WS_CREATE
)
return True
@callback
@websocket_api.require_owner
def websocket_list(hass, connection, msg):
"""Return a list of users."""
async def send_users():
"""Send users."""
result = [_user_info(u) for u in await hass.auth.async_get_users()]
connection.send_message_outside(
websocket_api.result_message(msg['id'], result))
hass.async_add_job(send_users())
@callback
@websocket_api.require_owner
def websocket_delete(hass, connection, msg):
"""Delete a user."""
async def delete_user():
"""Delete user."""
if msg['user_id'] == connection.request.get('hass_user').id:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'no_delete_self',
'Unable to delete your own account'))
return
user = await hass.auth.async_get_user(msg['user_id'])
if not user:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'not_found', 'User not found'))
return
await hass.auth.async_remove_user(user)
connection.send_message_outside(
websocket_api.result_message(msg['id']))
hass.async_add_job(delete_user())
@callback
@websocket_api.require_owner
def websocket_create(hass, connection, msg):
"""Create a user."""
async def create_user():
"""Create a user."""
user = await hass.auth.async_create_user(msg['name'])
connection.send_message_outside(
websocket_api.result_message(msg['id'], {
'user': _user_info(user)
}))
hass.async_add_job(create_user())
def _user_info(user):
"""Format a user."""
return {
'id': user.id,
'name': user.name,
'is_owner': user.is_owner,
'is_active': user.is_active,
'system_generated': user.system_generated,
'credentials': [
{
'type': c.auth_provider_type,
} for c in user.credentials
]
}

View File

@@ -0,0 +1,174 @@
"""Offer API to configure the Home Assistant auth provider."""
import voluptuous as vol
from homeassistant.auth.providers import homeassistant as auth_ha
from homeassistant.core import callback
from homeassistant.components import websocket_api
WS_TYPE_CREATE = 'config/auth_provider/homeassistant/create'
SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_CREATE,
vol.Required('user_id'): str,
vol.Required('username'): str,
vol.Required('password'): str,
})
WS_TYPE_DELETE = 'config/auth_provider/homeassistant/delete'
SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_DELETE,
vol.Required('username'): str,
})
WS_TYPE_CHANGE_PASSWORD = 'config/auth_provider/homeassistant/change_password'
SCHEMA_WS_CHANGE_PASSWORD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_CHANGE_PASSWORD,
vol.Required('current_password'): str,
vol.Required('new_password'): str
})
async def async_setup(hass):
"""Enable the Home Assistant views."""
hass.components.websocket_api.async_register_command(
WS_TYPE_CREATE, websocket_create,
SCHEMA_WS_CREATE
)
hass.components.websocket_api.async_register_command(
WS_TYPE_DELETE, websocket_delete,
SCHEMA_WS_DELETE
)
hass.components.websocket_api.async_register_command(
WS_TYPE_CHANGE_PASSWORD, websocket_change_password,
SCHEMA_WS_CHANGE_PASSWORD
)
return True
def _get_provider(hass):
"""Get homeassistant auth provider."""
for prv in hass.auth.auth_providers:
if prv.type == 'homeassistant':
return prv
raise RuntimeError('Provider not found')
@callback
@websocket_api.require_owner
def websocket_create(hass, connection, msg):
"""Create credentials and attach to a user."""
async def create_creds():
"""Create credentials."""
provider = _get_provider(hass)
await provider.async_initialize()
user = await hass.auth.async_get_user(msg['user_id'])
if user is None:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'not_found', 'User not found'))
return
if user.system_generated:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'system_generated',
'Cannot add credentials to a system generated user.'))
return
try:
await hass.async_add_executor_job(
provider.data.add_auth, msg['username'], msg['password'])
except auth_ha.InvalidUser:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'username_exists', 'Username already exists'))
return
credentials = await provider.async_get_or_create_credentials({
'username': msg['username']
})
await hass.auth.async_link_user(user, credentials)
await provider.data.async_save()
connection.to_write.put_nowait(websocket_api.result_message(msg['id']))
hass.async_add_job(create_creds())
@callback
@websocket_api.require_owner
def websocket_delete(hass, connection, msg):
"""Delete username and related credential."""
async def delete_creds():
"""Delete user credentials."""
provider = _get_provider(hass)
await provider.async_initialize()
credentials = await provider.async_get_or_create_credentials({
'username': msg['username']
})
# if not new, an existing credential exists.
# Removing the credential will also remove the auth.
if not credentials.is_new:
await hass.auth.async_remove_credentials(credentials)
connection.to_write.put_nowait(
websocket_api.result_message(msg['id']))
return
try:
provider.data.async_remove_auth(msg['username'])
await provider.data.async_save()
except auth_ha.InvalidUser:
connection.to_write.put_nowait(websocket_api.error_message(
msg['id'], 'auth_not_found', 'Given username was not found.'))
return
connection.to_write.put_nowait(
websocket_api.result_message(msg['id']))
hass.async_add_job(delete_creds())
@callback
def websocket_change_password(hass, connection, msg):
"""Change user password."""
async def change_password():
"""Change user password."""
user = connection.request.get('hass_user')
if user is None:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'user_not_found', 'User not found'))
return
provider = _get_provider(hass)
await provider.async_initialize()
username = None
for credential in user.credentials:
if credential.auth_provider_type == provider.type:
username = credential.data['username']
break
if username is None:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'credentials_not_found', 'Credentials not found'))
return
try:
await provider.async_validate_login(
username, msg['current_password'])
except auth_ha.InvalidAuth:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'invalid_password', 'Invalid password'))
return
await hass.async_add_executor_job(
provider.data.change_password, username, msg['new_password'])
await provider.data.async_save()
connection.send_message_outside(
websocket_api.result_message(msg['id']))
hass.async_add_job(change_password())

0
homeassistant/components/cover/group.py Executable file → Normal file
View File

View File

@@ -4,7 +4,6 @@ Support for Lutron Caseta shades.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.lutron_caseta/
"""
import asyncio
import logging
from homeassistant.components.cover import (
@@ -18,8 +17,8 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['lutron_caseta']
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the Lutron Caseta shades as a cover device."""
devs = []
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
@@ -49,25 +48,21 @@ class LutronCasetaCover(LutronCasetaDevice, CoverDevice):
"""Return the current position of cover."""
return self._state['current_state']
@asyncio.coroutine
def async_close_cover(self, **kwargs):
async def async_close_cover(self, **kwargs):
"""Close the cover."""
self._smartbridge.set_value(self._device_id, 0)
@asyncio.coroutine
def async_open_cover(self, **kwargs):
async def async_open_cover(self, **kwargs):
"""Open the cover."""
self._smartbridge.set_value(self._device_id, 100)
@asyncio.coroutine
def async_set_cover_position(self, **kwargs):
async def async_set_cover_position(self, **kwargs):
"""Move the shade to a specific position."""
if ATTR_POSITION in kwargs:
position = kwargs[ATTR_POSITION]
self._smartbridge.set_value(self._device_id, position)
@asyncio.coroutine
def async_update(self):
async def async_update(self):
"""Call when forcing a refresh of the device."""
self._state = self._smartbridge.get_device_by_id(self._device_id)
_LOGGER.debug(self._state)

View File

@@ -4,7 +4,6 @@ Support for MQTT cover devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.mqtt/
"""
import asyncio
import logging
import voluptuous as vol
@@ -93,8 +92,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the MQTT Cover."""
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
@@ -174,10 +173,9 @@ class MqttCover(MqttAvailability, CoverDevice):
self._position_topic = position_topic
self._set_position_template = set_position_template
@asyncio.coroutine
def async_added_to_hass(self):
async def async_added_to_hass(self):
"""Subscribe MQTT events."""
yield from super().async_added_to_hass()
await super().async_added_to_hass()
@callback
def tilt_updated(topic, payload, qos):
@@ -218,7 +216,7 @@ class MqttCover(MqttAvailability, CoverDevice):
# Force into optimistic mode.
self._optimistic = True
else:
yield from mqtt.async_subscribe(
await mqtt.async_subscribe(
self.hass, self._state_topic,
state_message_received, self._qos)
@@ -227,7 +225,7 @@ class MqttCover(MqttAvailability, CoverDevice):
else:
self._tilt_optimistic = False
self._tilt_value = STATE_UNKNOWN
yield from mqtt.async_subscribe(
await mqtt.async_subscribe(
self.hass, self._tilt_status_topic, tilt_updated, self._qos)
@property
@@ -278,8 +276,7 @@ class MqttCover(MqttAvailability, CoverDevice):
return supported_features
@asyncio.coroutine
def async_open_cover(self, **kwargs):
async def async_open_cover(self, **kwargs):
"""Move the cover up.
This method is a coroutine.
@@ -292,8 +289,7 @@ class MqttCover(MqttAvailability, CoverDevice):
self._state = False
self.async_schedule_update_ha_state()
@asyncio.coroutine
def async_close_cover(self, **kwargs):
async def async_close_cover(self, **kwargs):
"""Move the cover down.
This method is a coroutine.
@@ -306,8 +302,7 @@ class MqttCover(MqttAvailability, CoverDevice):
self._state = True
self.async_schedule_update_ha_state()
@asyncio.coroutine
def async_stop_cover(self, **kwargs):
async def async_stop_cover(self, **kwargs):
"""Stop the device.
This method is a coroutine.
@@ -316,8 +311,7 @@ class MqttCover(MqttAvailability, CoverDevice):
self.hass, self._command_topic, self._payload_stop, self._qos,
self._retain)
@asyncio.coroutine
def async_open_cover_tilt(self, **kwargs):
async def async_open_cover_tilt(self, **kwargs):
"""Tilt the cover open."""
mqtt.async_publish(self.hass, self._tilt_command_topic,
self._tilt_open_position, self._qos,
@@ -326,8 +320,7 @@ class MqttCover(MqttAvailability, CoverDevice):
self._tilt_value = self._tilt_open_position
self.async_schedule_update_ha_state()
@asyncio.coroutine
def async_close_cover_tilt(self, **kwargs):
async def async_close_cover_tilt(self, **kwargs):
"""Tilt the cover closed."""
mqtt.async_publish(self.hass, self._tilt_command_topic,
self._tilt_closed_position, self._qos,
@@ -336,8 +329,7 @@ class MqttCover(MqttAvailability, CoverDevice):
self._tilt_value = self._tilt_closed_position
self.async_schedule_update_ha_state()
@asyncio.coroutine
def async_set_cover_tilt_position(self, **kwargs):
async def async_set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
if ATTR_TILT_POSITION not in kwargs:
return
@@ -350,8 +342,7 @@ class MqttCover(MqttAvailability, CoverDevice):
mqtt.async_publish(self.hass, self._tilt_command_topic,
level, self._qos, self._retain)
@asyncio.coroutine
def async_set_cover_position(self, **kwargs):
async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
if ATTR_POSITION in kwargs:
position = kwargs[ATTR_POSITION]

View File

@@ -4,7 +4,6 @@ Support for Rflink Cover devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.rflink/
"""
import asyncio
import logging
import voluptuous as vol
@@ -79,8 +78,8 @@ def devices_from_config(domain_config, hass=None):
return devices
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the Rflink cover platform."""
async_add_devices(devices_from_config(config, hass))

View File

@@ -81,7 +81,11 @@ class TahomaCover(TahomaDevice, CoverDevice):
self.apply_action('setPosition', 'secured')
elif self.tahoma_device.type in \
('rts:BlindRTSComponent',
'io:ExteriorVenetianBlindIOComponent'):
'io:ExteriorVenetianBlindIOComponent',
'rts:VenetianBlindRTSComponent',
'rts:DualCurtainRTSComponent',
'rts:ExteriorVenetianBlindRTSComponent',
'rts:BlindRTSComponent'):
self.apply_action('my')
else:
self.apply_action('stopIdentify')

View File

@@ -4,7 +4,6 @@ Support for covers which integrate with other components.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.template/
"""
import asyncio
import logging
import voluptuous as vol
@@ -72,8 +71,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the Template cover."""
covers = []
@@ -199,8 +198,7 @@ class CoverTemplate(CoverDevice):
if self._entity_picture_template is not None:
self._entity_picture_template.hass = self.hass
@asyncio.coroutine
def async_added_to_hass(self):
async def async_added_to_hass(self):
"""Register callbacks."""
@callback
def template_cover_state_listener(entity, old_state, new_state):
@@ -277,70 +275,62 @@ class CoverTemplate(CoverDevice):
"""Return the polling state."""
return False
@asyncio.coroutine
def async_open_cover(self, **kwargs):
async def async_open_cover(self, **kwargs):
"""Move the cover up."""
if self._open_script:
yield from self._open_script.async_run()
await self._open_script.async_run()
elif self._position_script:
yield from self._position_script.async_run({"position": 100})
await self._position_script.async_run({"position": 100})
if self._optimistic:
self._position = 100
self.async_schedule_update_ha_state()
@asyncio.coroutine
def async_close_cover(self, **kwargs):
async def async_close_cover(self, **kwargs):
"""Move the cover down."""
if self._close_script:
yield from self._close_script.async_run()
await self._close_script.async_run()
elif self._position_script:
yield from self._position_script.async_run({"position": 0})
await self._position_script.async_run({"position": 0})
if self._optimistic:
self._position = 0
self.async_schedule_update_ha_state()
@asyncio.coroutine
def async_stop_cover(self, **kwargs):
async def async_stop_cover(self, **kwargs):
"""Fire the stop action."""
if self._stop_script:
yield from self._stop_script.async_run()
await self._stop_script.async_run()
@asyncio.coroutine
def async_set_cover_position(self, **kwargs):
async def async_set_cover_position(self, **kwargs):
"""Set cover position."""
self._position = kwargs[ATTR_POSITION]
yield from self._position_script.async_run(
await self._position_script.async_run(
{"position": self._position})
if self._optimistic:
self.async_schedule_update_ha_state()
@asyncio.coroutine
def async_open_cover_tilt(self, **kwargs):
async def async_open_cover_tilt(self, **kwargs):
"""Tilt the cover open."""
self._tilt_value = 100
yield from self._tilt_script.async_run({"tilt": self._tilt_value})
await self._tilt_script.async_run({"tilt": self._tilt_value})
if self._tilt_optimistic:
self.async_schedule_update_ha_state()
@asyncio.coroutine
def async_close_cover_tilt(self, **kwargs):
async def async_close_cover_tilt(self, **kwargs):
"""Tilt the cover closed."""
self._tilt_value = 0
yield from self._tilt_script.async_run(
await self._tilt_script.async_run(
{"tilt": self._tilt_value})
if self._tilt_optimistic:
self.async_schedule_update_ha_state()
@asyncio.coroutine
def async_set_cover_tilt_position(self, **kwargs):
async def async_set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
self._tilt_value = kwargs[ATTR_TILT_POSITION]
yield from self._tilt_script.async_run({"tilt": self._tilt_value})
await self._tilt_script.async_run({"tilt": self._tilt_value})
if self._tilt_optimistic:
self.async_schedule_update_ha_state()
@asyncio.coroutine
def async_update(self):
async def async_update(self):
"""Update the state from the template."""
if self._template is not None:
try:

View File

@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.velbus/
"""
import logging
import asyncio
import time
import voluptuous as vol
@@ -70,15 +69,14 @@ class VelbusCover(CoverDevice):
self._open_channel = open_channel
self._close_channel = close_channel
@asyncio.coroutine
def async_added_to_hass(self):
async def async_added_to_hass(self):
"""Add listener for Velbus messages on bus."""
def _init_velbus():
"""Initialize Velbus on startup."""
self._velbus.subscribe(self._on_message)
self.get_status()
yield from self.hass.async_add_job(_init_velbus)
await self.hass.async_add_job(_init_velbus)
def _on_message(self, message):
import velbus

View File

@@ -4,8 +4,6 @@ Support for Wink Covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.wink/
"""
import asyncio
from homeassistant.components.cover import CoverDevice, STATE_UNKNOWN, \
ATTR_POSITION
from homeassistant.components.wink import WinkDevice, DOMAIN
@@ -34,8 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class WinkCoverDevice(WinkDevice, CoverDevice):
"""Representation of a Wink cover device."""
@asyncio.coroutine
def async_added_to_hass(self):
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
self.hass.data[DOMAIN]['entities']['cover'].append(self)

View File

@@ -22,7 +22,7 @@ from .const import (
CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER)
REQUIREMENTS = ['pydeconz==39']
REQUIREMENTS = ['pydeconz==42']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({

View File

@@ -11,3 +11,6 @@ DATA_DECONZ_UNSUB = 'deconz_dispatchers'
CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor'
CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
ATTR_DARK = 'dark'
ATTR_ON = 'on'

View File

@@ -16,7 +16,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pexpect==4.0.1']
REQUIREMENTS = ['pexpect==4.6.0']
_DEVICES_REGEX = re.compile(
r'(?P<name>([^\s]+)?)\s+' +

View File

@@ -19,7 +19,7 @@ from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_MODE,
CONF_PROTOCOL)
REQUIREMENTS = ['pexpect==4.0.1']
REQUIREMENTS = ['pexpect==4.6.0']
_LOGGER = logging.getLogger(__name__)
@@ -311,12 +311,11 @@ class SshConnection(_Connection):
super().connect()
def disconnect(self): \
# pylint: disable=broad-except
def disconnect(self):
"""Disconnect the current SSH connection."""
try:
self._ssh.logout()
except Exception:
except Exception: # pylint: disable=broad-except
pass
finally:
self._ssh = None
@@ -379,12 +378,11 @@ class TelnetConnection(_Connection):
super().connect()
def disconnect(self): \
# pylint: disable=broad-except
def disconnect(self):
"""Disconnect the current Telnet connection."""
try:
self._telnet.write('exit\n'.encode('ascii'))
except Exception:
except Exception: # pylint: disable=broad-except
pass
super().disconnect()

View File

@@ -16,7 +16,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pexpect==4.0.1']
REQUIREMENTS = ['pexpect==4.6.0']
PLATFORM_SCHEMA = vol.All(
PLATFORM_SCHEMA.extend({

View File

@@ -66,7 +66,6 @@ class MikrotikScanner(DeviceScanner):
def connect_to_device(self):
"""Connect to Mikrotik method."""
# pylint: disable=import-error
import librouteros
try:
self.client = librouteros.connect(

View File

@@ -5,24 +5,22 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.tile/
"""
import logging
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_USERNAME, CONF_MONITORED_VARIABLES, CONF_PASSWORD)
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util import slugify
from homeassistant.util.json import load_json, save_json
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pytile==1.1.0']
REQUIREMENTS = ['pytile==2.0.2']
CLIENT_UUID_CONFIG_FILE = '.tile.conf'
DEFAULT_ICON = 'mdi:bluetooth'
DEVICE_TYPES = ['PHONE', 'TILE']
ATTR_ALTITUDE = 'altitude'
@@ -34,89 +32,111 @@ ATTR_VOIP_STATE = 'voip_state'
CONF_SHOW_INACTIVE = 'show_inactive'
DEFAULT_ICON = 'mdi:bluetooth'
DEFAULT_SCAN_INTERVAL = timedelta(minutes=2)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SHOW_INACTIVE, default=False): cv.boolean,
vol.Optional(CONF_MONITORED_VARIABLES):
vol.Optional(CONF_MONITORED_VARIABLES, default=DEVICE_TYPES):
vol.All(cv.ensure_list, [vol.In(DEVICE_TYPES)]),
})
def setup_scanner(hass, config: dict, see, discovery_info=None):
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Validate the configuration and return a Tile scanner."""
TileDeviceScanner(hass, config, see)
return True
from pytile import Client
websession = aiohttp_client.async_get_clientsession(hass)
config_data = await hass.async_add_job(
load_json, hass.config.path(CLIENT_UUID_CONFIG_FILE))
if config_data:
client = Client(
config[CONF_USERNAME],
config[CONF_PASSWORD],
websession,
client_uuid=config_data['client_uuid'])
else:
client = Client(
config[CONF_USERNAME], config[CONF_PASSWORD], websession)
config_data = {'client_uuid': client.client_uuid}
config_saved = await hass.async_add_job(
save_json, hass.config.path(CLIENT_UUID_CONFIG_FILE), config_data)
if not config_saved:
_LOGGER.error('Failed to save the client UUID')
scanner = TileScanner(
client, hass, async_see, config[CONF_MONITORED_VARIABLES],
config[CONF_SHOW_INACTIVE])
return await scanner.async_init()
class TileDeviceScanner(DeviceScanner):
"""Define a device scanner for Tiles."""
class TileScanner(object):
"""Define an object to retrieve Tile data."""
def __init__(self, hass, config, see):
def __init__(self, client, hass, async_see, types, show_inactive):
"""Initialize."""
from pytile import Client
self._async_see = async_see
self._client = client
self._hass = hass
self._show_inactive = show_inactive
self._types = types
_LOGGER.debug('Received configuration data: %s', config)
async def async_init(self):
"""Further initialize connection to the Tile servers."""
from pytile.errors import TileError
# Load the client UUID (if it exists):
config_data = load_json(hass.config.path(CLIENT_UUID_CONFIG_FILE))
if config_data:
_LOGGER.debug('Using existing client UUID')
self._client = Client(
config[CONF_USERNAME],
config[CONF_PASSWORD],
config_data['client_uuid'])
else:
_LOGGER.debug('Generating new client UUID')
self._client = Client(
config[CONF_USERNAME],
config[CONF_PASSWORD])
try:
await self._client.async_init()
except TileError as err:
_LOGGER.error('Unable to set up Tile scanner: %s', err)
return False
if not save_json(
hass.config.path(CLIENT_UUID_CONFIG_FILE),
{'client_uuid': self._client.client_uuid}):
_LOGGER.error("Failed to save configuration file")
await self._async_update()
_LOGGER.debug('Client UUID: %s', self._client.client_uuid)
_LOGGER.debug('User UUID: %s', self._client.user_uuid)
async_track_time_interval(
self._hass, self._async_update, DEFAULT_SCAN_INTERVAL)
self._show_inactive = config.get(CONF_SHOW_INACTIVE)
self._types = config.get(CONF_MONITORED_VARIABLES)
return True
self.devices = {}
self.see = see
async def _async_update(self, now=None):
"""Update info from Tile."""
from pytile.errors import SessionExpiredError, TileError
track_utc_time_change(
hass, self._update_info, second=range(0, 60, 30))
_LOGGER.debug('Updating Tile data')
self._update_info()
try:
await self._client.asayn_init()
tiles = await self._client.tiles.all(
whitelist=self._types, show_inactive=self._show_inactive)
except SessionExpiredError:
_LOGGER.info('Session expired; trying again shortly')
return
except TileError as err:
_LOGGER.error('There was an error while updating: %s', err)
return
def _update_info(self, now=None) -> None:
"""Update the device info."""
self.devices = self._client.get_tiles(
type_whitelist=self._types, show_inactive=self._show_inactive)
if not self.devices:
if not tiles:
_LOGGER.warning('No Tiles found')
return
for dev in self.devices:
dev_id = 'tile_{0}'.format(slugify(dev['name']))
lat = dev['tileState']['latitude']
lon = dev['tileState']['longitude']
attrs = {
ATTR_ALTITUDE: dev['tileState']['altitude'],
ATTR_CONNECTION_STATE: dev['tileState']['connection_state'],
ATTR_IS_DEAD: dev['is_dead'],
ATTR_IS_LOST: dev['tileState']['is_lost'],
ATTR_RING_STATE: dev['tileState']['ring_state'],
ATTR_VOIP_STATE: dev['tileState']['voip_state'],
}
self.see(
dev_id=dev_id,
gps=(lat, lon),
attributes=attrs,
icon=DEFAULT_ICON
)
for tile in tiles:
await self._async_see(
dev_id='tile_{0}'.format(slugify(tile['name'])),
gps=(
tile['tileState']['latitude'],
tile['tileState']['longitude']
),
attributes={
ATTR_ALTITUDE: tile['tileState']['altitude'],
ATTR_CONNECTION_STATE:
tile['tileState']['connection_state'],
ATTR_IS_DEAD: tile['is_dead'],
ATTR_IS_LOST: tile['tileState']['is_lost'],
ATTR_RING_STATE: tile['tileState']['ring_state'],
ATTR_VOIP_STATE: tile['tileState']['voip_state'],
},
icon=DEFAULT_ICON)

View File

@@ -16,7 +16,7 @@ from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
CONF_PORT)
REQUIREMENTS = ['pexpect==4.0.1']
REQUIREMENTS = ['pexpect==4.6.0']
_LOGGER = logging.getLogger(__name__)

View File

@@ -64,7 +64,7 @@ class XiaomiMiioDeviceScanner(DeviceScanner):
station_info = await self.hass.async_add_job(self.device.status)
_LOGGER.debug("Got new station info: %s", station_info)
for device in station_info['mat']:
for device in station_info.associated_stations:
devices.append(device['mac'])
except DeviceException as ex:

View File

@@ -99,7 +99,8 @@ async def async_handle_message(hass, message):
return None
action = req.get('action', '')
parameters = req.get('parameters')
parameters = req.get('parameters').copy()
parameters["dialogflow_query"] = message
dialogflow_response = DialogflowResponse(parameters)
if action == "":

View File

@@ -6,6 +6,7 @@ https://home-assistant.io/components/emulated_hue/
"""
import logging
from aiohttp import web
import voluptuous as vol
from homeassistant import util
@@ -13,7 +14,6 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.components.http import REQUIREMENTS # NOQA
from homeassistant.components.http import HomeAssistantHTTP
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.deprecation import get_deprecated
import homeassistant.helpers.config_validation as cv
@@ -85,28 +85,17 @@ def setup(hass, yaml_config):
"""Activate the emulated_hue component."""
config = Config(hass, yaml_config.get(DOMAIN, {}))
server = HomeAssistantHTTP(
hass,
server_host=config.host_ip_addr,
server_port=config.listen_port,
api_password=None,
ssl_certificate=None,
ssl_peer_certificate=None,
ssl_key=None,
cors_origins=None,
use_x_forwarded_for=False,
trusted_proxies=[],
trusted_networks=[],
login_threshold=0,
is_ban_enabled=False
)
app = web.Application()
app['hass'] = hass
handler = None
server = None
server.register_view(DescriptionXmlView(config))
server.register_view(HueUsernameView)
server.register_view(HueAllLightsStateView(config))
server.register_view(HueOneLightStateView(config))
server.register_view(HueOneLightChangeView(config))
server.register_view(HueGroupView(config))
DescriptionXmlView(config).register(app, app.router)
HueUsernameView().register(app, app.router)
HueAllLightsStateView(config).register(app, app.router)
HueOneLightStateView(config).register(app, app.router)
HueOneLightChangeView(config).register(app, app.router)
HueGroupView(config).register(app, app.router)
upnp_listener = UPNPResponderThread(
config.host_ip_addr, config.listen_port,
@@ -116,14 +105,31 @@ def setup(hass, yaml_config):
async def stop_emulated_hue_bridge(event):
"""Stop the emulated hue bridge."""
upnp_listener.stop()
await server.stop()
if server:
server.close()
await server.wait_closed()
await app.shutdown()
if handler:
await handler.shutdown(10)
await app.cleanup()
async def start_emulated_hue_bridge(event):
"""Start the emulated hue bridge."""
upnp_listener.start()
await server.start()
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge)
nonlocal handler
nonlocal server
handler = app.make_handler(loop=hass.loop)
try:
server = await hass.loop.create_server(
handler, config.host_ip_addr, config.listen_port)
except OSError as error:
_LOGGER.error("Failed to create HTTP server at port %d: %s",
config.listen_port, error)
else:
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge)

View File

@@ -75,6 +75,7 @@ class EnOceanDongle:
_LOGGER.debug("Received radio packet: %s", temp)
rxtype = None
value = None
channel = 0
if temp.data[6] == 0x30:
rxtype = "wallswitch"
value = 1
@@ -84,8 +85,9 @@ class EnOceanDongle:
elif temp.data[4] == 0x0c:
rxtype = "power"
value = temp.data[3] + (temp.data[2] << 8)
elif temp.data[2] == 0x60:
elif temp.data[2] & 0x60 == 0x60:
rxtype = "switch_status"
channel = temp.data[2] & 0x1F
if temp.data[3] == 0xe4:
value = 1
elif temp.data[3] == 0x80:
@@ -104,7 +106,8 @@ class EnOceanDongle:
if temp.sender_int == self._combine_hex(device.dev_id):
if value > 10:
device.value_changed(1)
if rxtype == "switch_status" and device.stype == "switch":
if rxtype == "switch_status" and device.stype == "switch" and \
channel == device.channel:
if temp.sender_int == self._combine_hex(device.dev_id):
device.value_changed(value)
if rxtype == "dimmerstatus" and device.stype == "dimmer":

View File

@@ -49,7 +49,6 @@ EUFY_DISPATCH = {
def setup(hass, config):
"""Set up Eufy devices."""
# pylint: disable=import-error
import lakeside
if CONF_USERNAME in config[DOMAIN] and CONF_PASSWORD in config[DOMAIN]:

0
homeassistant/components/fritzbox.py Executable file → Normal file
View File

View File

@@ -26,10 +26,10 @@ from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
from homeassistant.util.yaml import load_yaml
REQUIREMENTS = ['home-assistant-frontend==20180702.1']
REQUIREMENTS = ['home-assistant-frontend==20180720.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', 'onboarding']
CONF_THEMES = 'themes'
CONF_EXTRA_HTML_URL = 'extra_html_url'
@@ -50,7 +50,7 @@ MANIFEST_JSON = {
'lang': 'en-US',
'name': 'Home Assistant',
'short_name': 'Assistant',
'start_url': '/states',
'start_url': '/?homescreen=1',
'theme_color': DEFAULT_THEME_COLOR
}
@@ -200,15 +200,6 @@ def add_manifest_json_key(key, val):
async def async_setup(hass, config):
"""Set up the serving of the frontend."""
if hass.auth.active:
client = await hass.auth.async_get_or_create_client(
'Home Assistant Frontend',
redirect_uris=['/'],
no_secret=True,
)
else:
client = None
hass.components.websocket_api.async_register_command(
WS_TYPE_GET_PANELS, websocket_get_panels, SCHEMA_GET_PANELS)
hass.components.websocket_api.async_register_command(
@@ -255,7 +246,7 @@ async def async_setup(hass, config):
if os.path.isdir(local):
hass.http.register_static_path("/local", local, not is_dev)
index_view = IndexView(repo_path, js_version, client)
index_view = IndexView(repo_path, js_version, hass.auth.active)
hass.http.register_view(index_view)
@callback
@@ -266,7 +257,7 @@ async def async_setup(hass, config):
await asyncio.wait(
[async_register_built_in_panel(hass, panel) for panel in (
'dev-event', 'dev-info', 'dev-service', 'dev-state',
'dev-template', 'dev-mqtt', 'kiosk', 'lovelace')],
'dev-template', 'dev-mqtt', 'kiosk', 'lovelace', 'profile')],
loop=hass.loop)
hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel
@@ -350,11 +341,11 @@ class IndexView(HomeAssistantView):
requires_auth = False
extra_urls = ['/states', '/states/{extra}']
def __init__(self, repo_path, js_option, client):
def __init__(self, repo_path, js_option, auth_active):
"""Initialize the frontend view."""
self.repo_path = repo_path
self.js_option = js_option
self.client = client
self.auth_active = auth_active
self._template_cache = {}
def get_template(self, latest):
@@ -386,11 +377,23 @@ class IndexView(HomeAssistantView):
latest = self.repo_path is not None or \
_is_latest(self.js_option, request)
if not hass.components.onboarding.async_is_onboarded():
if latest:
location = '/frontend_latest/onboarding.html'
else:
location = '/frontend_es5/onboarding.html'
return web.Response(status=302, headers={
'location': location
})
no_auth = '1'
if hass.config.api.api_password and not request[KEY_AUTHENTICATED]:
# do not try to auto connect on load
no_auth = '0'
use_oauth = '1' if self.auth_active else '0'
template = await hass.async_add_job(self.get_template, latest)
extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5
@@ -399,11 +402,9 @@ class IndexView(HomeAssistantView):
no_auth=no_auth,
theme_color=MANIFEST_JSON['theme_color'],
extra_urls=hass.data[extra_key],
use_oauth=use_oauth
)
if self.client is not None:
template_params['client_id'] = self.client.id
return web.Response(text=template.render(**template_params),
content_type='text/html')
@@ -489,7 +490,7 @@ def websocket_get_translations(hass, connection, msg):
Async friendly.
"""
async def send_translations():
"""Send a camera still."""
"""Send a translation."""
resources = await async_get_translations(hass, msg['language'])
connection.send_message_outside(websocket_api.result_message(
msg['id'], {

View File

@@ -25,6 +25,7 @@ from homeassistant.util import convert, dt
REQUIREMENTS = [
'google-api-python-client==1.6.4',
'httplib2==0.10.3',
'oauth2client==4.0.0',
]

View File

@@ -10,10 +10,8 @@ from aiohttp.hdrs import AUTHORIZATION
from aiohttp.web import Request, Response
# Typing imports
# pylint: disable=unused-import
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import HomeAssistant, callback # NOQA
from homeassistant.helpers.entity import Entity # NOQA
from homeassistant.core import callback
from .const import (
GOOGLE_ASSISTANT_API_ENDPOINT,

View File

@@ -3,14 +3,6 @@ import collections
from itertools import product
import logging
# Typing imports
# pylint: disable=unused-import
# if False:
from aiohttp.web import Request, Response # NOQA
from typing import Dict, Tuple, Any, Optional # NOQA
from homeassistant.helpers.entity import Entity # NOQA
from homeassistant.core import HomeAssistant # NOQA
from homeassistant.util.unit_system import UnitSystem # NOQA
from homeassistant.util.decorator import Registry
from homeassistant.core import callback

View File

@@ -14,7 +14,7 @@ from homeassistant.components.discovery import SERVICE_HOMEKIT
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['homekit==0.6']
REQUIREMENTS = ['homekit==0.10']
DOMAIN = 'homekit_controller'
HOMEKIT_DIR = '.homekit'
@@ -26,6 +26,12 @@ HOMEKIT_ACCESSORY_DISPATCH = {
'thermostat': 'climate',
}
HOMEKIT_IGNORE = [
'BSB002',
'Home Assistant Bridge',
'TRADFRI gateway'
]
KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN)
KNOWN_DEVICES = "{}-devices".format(DOMAIN)
@@ -237,6 +243,9 @@ def setup(hass, config):
hkid = discovery_info['properties']['id']
config_num = int(discovery_info['properties']['c#'])
if model in HOMEKIT_IGNORE:
return
# Only register a device once, but rescan if the config has changed
if hkid in hass.data[KNOWN_DEVICES]:
device = hass.data[KNOWN_DEVICES][hkid]

View File

@@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.loader import bind_hass
REQUIREMENTS = ['pyhomematic==0.1.44']
REQUIREMENTS = ['pyhomematic==0.1.45']
_LOGGER = logging.getLogger(__name__)
@@ -71,7 +71,7 @@ HM_DEVICE_TYPES = {
'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch',
'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall',
'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat',
'IPWeatherSensor', 'RotaryHandleSensorIP'],
'IPWeatherSensor', 'RotaryHandleSensorIP', 'IPPassageSensor'],
DISCOVER_CLIMATE: [
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
@@ -80,7 +80,7 @@ HM_DEVICE_TYPES = {
'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor',
'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain',
'WiredSensor', 'PresenceIP', 'IPWeatherSensor'],
'WiredSensor', 'PresenceIP', 'IPWeatherSensor', 'IPPassageSensor'],
DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'],
DISCOVER_LOCKS: ['KeyMatic']
}
@@ -114,7 +114,7 @@ HM_ATTRIBUTE_SUPPORT = {
'CURRENT': ['current', {}],
'VOLTAGE': ['voltage', {}],
'OPERATING_VOLTAGE': ['voltage', {}],
'WORKING': ['working', {0: 'No', 1: 'Yes'}],
'WORKING': ['working', {0: 'No', 1: 'Yes'}]
}
HM_PRESS_EVENTS = [

View File

@@ -1,262 +0,0 @@
"""
Support for HomematicIP components.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/homematicip_cloud/
"""
import asyncio
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.entity import Entity
from homeassistant.core import callback
REQUIREMENTS = ['homematicip==0.9.4']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'homematicip_cloud'
COMPONENTS = [
'sensor',
'binary_sensor',
'switch',
'light',
'climate',
]
CONF_NAME = 'name'
CONF_ACCESSPOINT = 'accesspoint'
CONF_AUTHTOKEN = 'authtoken'
CONFIG_SCHEMA = vol.Schema({
vol.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({
vol.Optional(CONF_NAME): vol.Any(cv.string),
vol.Required(CONF_ACCESSPOINT): cv.string,
vol.Required(CONF_AUTHTOKEN): cv.string,
})]),
}, extra=vol.ALLOW_EXTRA)
HMIP_ACCESS_POINT = 'Access Point'
HMIP_HUB = 'HmIP-HUB'
ATTR_HOME_ID = 'home_id'
ATTR_HOME_NAME = 'home_name'
ATTR_DEVICE_ID = 'device_id'
ATTR_DEVICE_LABEL = 'device_label'
ATTR_STATUS_UPDATE = 'status_update'
ATTR_FIRMWARE_STATE = 'firmware_state'
ATTR_UNREACHABLE = 'unreachable'
ATTR_LOW_BATTERY = 'low_battery'
ATTR_MODEL_TYPE = 'model_type'
ATTR_GROUP_TYPE = 'group_type'
ATTR_DEVICE_RSSI = 'device_rssi'
ATTR_DUTY_CYCLE = 'duty_cycle'
ATTR_CONNECTED = 'connected'
ATTR_SABOTAGE = 'sabotage'
ATTR_OPERATION_LOCK = 'operation_lock'
async def async_setup(hass, config):
"""Set up the HomematicIP component."""
from homematicip.base.base_connection import HmipConnectionError
hass.data.setdefault(DOMAIN, {})
accesspoints = config.get(DOMAIN, [])
for conf in accesspoints:
_websession = async_get_clientsession(hass)
_hmip = HomematicipConnector(hass, conf, _websession)
try:
await _hmip.init()
except HmipConnectionError:
_LOGGER.error('Failed to connect to the HomematicIP server, %s.',
conf.get(CONF_ACCESSPOINT))
return False
home = _hmip.home
home.name = conf.get(CONF_NAME)
home.label = HMIP_ACCESS_POINT
home.modelType = HMIP_HUB
hass.data[DOMAIN][home.id] = home
_LOGGER.info('Connected to the HomematicIP server, %s.',
conf.get(CONF_ACCESSPOINT))
homeid = {ATTR_HOME_ID: home.id}
for component in COMPONENTS:
hass.async_add_job(async_load_platform(hass, component, DOMAIN,
homeid, config))
hass.loop.create_task(_hmip.connect())
return True
class HomematicipConnector:
"""Manages HomematicIP http and websocket connection."""
def __init__(self, hass, config, websession):
"""Initialize HomematicIP cloud connection."""
from homematicip.async.home import AsyncHome
self._hass = hass
self._ws_close_requested = False
self._retry_task = None
self._tries = 0
self._accesspoint = config.get(CONF_ACCESSPOINT)
_authtoken = config.get(CONF_AUTHTOKEN)
self.home = AsyncHome(hass.loop, websession)
self.home.set_auth_token(_authtoken)
self.home.on_update(self.async_update)
self._accesspoint_connected = True
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.close())
async def init(self):
"""Initialize connection."""
await self.home.init(self._accesspoint)
await self.home.get_current_state()
@callback
def async_update(self, *args, **kwargs):
"""Async update the home device.
Triggered when the hmip HOME_CHANGED event has fired.
There are several occasions for this event to happen.
We are only interested to check whether the access point
is still connected. If not, device state changes cannot
be forwarded to hass. So if access point is disconnected all devices
are set to unavailable.
"""
if not self.home.connected:
_LOGGER.error(
"HMIP access point has lost connection with the cloud")
self._accesspoint_connected = False
self.set_all_to_unavailable()
elif not self._accesspoint_connected:
# Explicitly getting an update as device states might have
# changed during access point disconnect."""
job = self._hass.async_add_job(self.get_state())
job.add_done_callback(self.get_state_finished)
async def get_state(self):
"""Update hmip state and tell hass."""
await self.home.get_current_state()
self.update_all()
def get_state_finished(self, future):
"""Execute when get_state coroutine has finished."""
from homematicip.base.base_connection import HmipConnectionError
try:
future.result()
except HmipConnectionError:
# Somehow connection could not recover. Will disconnect and
# so reconnect loop is taking over.
_LOGGER.error(
"updating state after himp access point reconnect failed.")
self._hass.async_add_job(self.home.disable_events())
def set_all_to_unavailable(self):
"""Set all devices to unavailable and tell Hass."""
for device in self.home.devices:
device.unreach = True
self.update_all()
def update_all(self):
"""Signal all devices to update their state."""
for device in self.home.devices:
device.fire_update_event()
async def _handle_connection(self):
"""Handle websocket connection."""
from homematicip.base.base_connection import HmipConnectionError
await self.home.get_current_state()
hmip_events = await self.home.enable_events()
try:
await hmip_events
except HmipConnectionError:
return
async def connect(self):
"""Start websocket connection."""
self._tries = 0
while True:
await self._handle_connection()
if self._ws_close_requested:
break
self._ws_close_requested = False
self._tries += 1
try:
self._retry_task = self._hass.async_add_job(asyncio.sleep(
2 ** min(9, self._tries), loop=self._hass.loop))
await self._retry_task
except asyncio.CancelledError:
break
_LOGGER.info('Reconnect (%s) to the HomematicIP cloud server.',
self._tries)
async def close(self):
"""Close the websocket connection."""
self._ws_close_requested = True
if self._retry_task is not None:
self._retry_task.cancel()
await self.home.disable_events()
_LOGGER.info("Closed connection to HomematicIP cloud server.")
class HomematicipGenericDevice(Entity):
"""Representation of an HomematicIP generic device."""
def __init__(self, home, device, post=None):
"""Initialize the generic device."""
self._home = home
self._device = device
self.post = post
_LOGGER.info('Setting up %s (%s)', self.name,
self._device.modelType)
async def async_added_to_hass(self):
"""Register callbacks."""
self._device.on_update(self._device_changed)
def _device_changed(self, json, **kwargs):
"""Handle device state changes."""
_LOGGER.debug('Event %s (%s)', self.name, self._device.modelType)
self.async_schedule_update_ha_state()
@property
def name(self):
"""Return the name of the generic device."""
name = self._device.label
if self._home.name is not None:
name = "{} {}".format(self._home.name, name)
if self.post is not None:
name = "{} {}".format(name, self.post)
return name
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def available(self):
"""Device available."""
return not self._device.unreach
@property
def device_state_attributes(self):
"""Return the state attributes of the generic device."""
return {
ATTR_LOW_BATTERY: self._device.lowBat,
ATTR_MODEL_TYPE: self._device.modelType
}

View File

@@ -0,0 +1,30 @@
{
"config": {
"title": "HomematicIP Cloud",
"step": {
"init": {
"title": "Pick HomematicIP Accesspoint",
"data": {
"hapid": "Accesspoint ID (SGTIN)",
"pin": "Pin Code (optional)",
"name": "Name (optional, used as name prefix for all devices)"
}
},
"link": {
"title": "Link Accesspoint",
"description": "Press the blue button on the accesspoint and the submit button to register HomematicIP with Home Assistant.\n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)"
}
},
"error": {
"register_failed": "Failed to register, please try again.",
"invalid_pin": "Invalid PIN, please try again.",
"press_the_button": "Please press the blue button.",
"timeout_button": "Blue button press timeout, please try again."
},
"abort": {
"unknown": "Unknown error occurred.",
"conection_aborted": "Could not connect to HMIP server",
"already_configured": "Accesspoint is already configured"
}
}
}

View File

@@ -0,0 +1,65 @@
"""
Support for HomematicIP components.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/homematicip_cloud/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from .const import (
DOMAIN, HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_NAME,
CONF_ACCESSPOINT, CONF_AUTHTOKEN, CONF_NAME)
# Loading the config flow file will register the flow
from .config_flow import configured_haps
from .hap import HomematicipHAP, HomematicipAuth # noqa: F401
from .device import HomematicipGenericDevice # noqa: F401
REQUIREMENTS = ['homematicip==0.9.8']
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({
vol.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({
vol.Optional(CONF_NAME, default=''): vol.Any(cv.string),
vol.Required(CONF_ACCESSPOINT): cv.string,
vol.Required(CONF_AUTHTOKEN): cv.string,
})]),
}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass, config):
"""Set up the HomematicIP component."""
hass.data[DOMAIN] = {}
accesspoints = config.get(DOMAIN, [])
for conf in accesspoints:
if conf[CONF_ACCESSPOINT] not in configured_haps(hass):
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source='import', data={
HMIPC_HAPID: conf[CONF_ACCESSPOINT],
HMIPC_AUTHTOKEN: conf[CONF_AUTHTOKEN],
HMIPC_NAME: conf[CONF_NAME],
}
))
return True
async def async_setup_entry(hass, entry):
"""Set up an accsspoint from a config entry."""
hap = HomematicipHAP(hass, entry)
hapid = entry.data[HMIPC_HAPID].replace('-', '').upper()
hass.data[DOMAIN][hapid] = hap
return await hap.async_setup()
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
hap = hass.data[DOMAIN].pop(entry.data[HMIPC_HAPID])
return await hap.async_reset()

View File

@@ -0,0 +1,97 @@
"""Config flow to configure HomematicIP Cloud."""
import voluptuous as vol
from homeassistant import config_entries, data_entry_flow
from homeassistant.core import callback
from .const import (
DOMAIN as HMIPC_DOMAIN, _LOGGER,
HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_PIN, HMIPC_NAME)
from .hap import HomematicipAuth
@callback
def configured_haps(hass):
"""Return a set of the configured accesspoints."""
return set(entry.data[HMIPC_HAPID] for entry
in hass.config_entries.async_entries(HMIPC_DOMAIN))
@config_entries.HANDLERS.register(HMIPC_DOMAIN)
class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler):
"""Config flow HomematicIP Cloud."""
VERSION = 1
def __init__(self):
"""Initialize HomematicIP Cloud config flow."""
self.auth = None
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
errors = {}
if user_input is not None:
user_input[HMIPC_HAPID] = \
user_input[HMIPC_HAPID].replace('-', '').upper()
if user_input[HMIPC_HAPID] in configured_haps(self.hass):
return self.async_abort(reason='already_configured')
self.auth = HomematicipAuth(self.hass, user_input)
connected = await self.auth.async_setup()
if connected:
_LOGGER.info("Connection established")
return await self.async_step_link()
return self.async_show_form(
step_id='init',
data_schema=vol.Schema({
vol.Required(HMIPC_HAPID): str,
vol.Optional(HMIPC_PIN): str,
vol.Optional(HMIPC_NAME): str,
}),
errors=errors
)
async def async_step_link(self, user_input=None):
"""Attempt to link with the HomematicIP Cloud accesspoint."""
errors = {}
pressed = await self.auth.async_checkbutton()
if pressed:
authtoken = await self.auth.async_register()
if authtoken:
_LOGGER.info("Write config entry")
return self.async_create_entry(
title=self.auth.config.get(HMIPC_HAPID),
data={
HMIPC_HAPID: self.auth.config.get(HMIPC_HAPID),
HMIPC_AUTHTOKEN: authtoken,
HMIPC_NAME: self.auth.config.get(HMIPC_NAME)
})
return self.async_abort(reason='conection_aborted')
else:
errors['base'] = 'press_the_button'
return self.async_show_form(step_id='link', errors=errors)
async def async_step_import(self, import_info):
"""Import a new bridge as a config entry."""
hapid = import_info[HMIPC_HAPID]
authtoken = import_info[HMIPC_AUTHTOKEN]
name = import_info[HMIPC_NAME]
hapid = hapid.replace('-', '').upper()
if hapid in configured_haps(self.hass):
return self.async_abort(reason='already_configured')
_LOGGER.info('Imported authentication for %s', hapid)
return self.async_create_entry(
title=hapid,
data={
HMIPC_HAPID: hapid,
HMIPC_AUTHTOKEN: authtoken,
HMIPC_NAME: name
}
)

View File

@@ -0,0 +1,24 @@
"""Constants for the HomematicIP Cloud component."""
import logging
_LOGGER = logging.getLogger('homeassistant.components.homematicip_cloud')
DOMAIN = 'homematicip_cloud'
COMPONENTS = [
'alarm_control_panel',
'binary_sensor',
'climate',
'light',
'sensor',
'switch',
]
CONF_NAME = 'name'
CONF_ACCESSPOINT = 'accesspoint'
CONF_AUTHTOKEN = 'authtoken'
HMIPC_NAME = 'name'
HMIPC_HAPID = 'hapid'
HMIPC_AUTHTOKEN = 'authtoken'
HMIPC_PIN = 'pin'

View File

@@ -0,0 +1,71 @@
"""GenericDevice for the HomematicIP Cloud component."""
import logging
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
ATTR_HOME_ID = 'home_id'
ATTR_HOME_NAME = 'home_name'
ATTR_DEVICE_ID = 'device_id'
ATTR_DEVICE_LABEL = 'device_label'
ATTR_STATUS_UPDATE = 'status_update'
ATTR_FIRMWARE_STATE = 'firmware_state'
ATTR_UNREACHABLE = 'unreachable'
ATTR_LOW_BATTERY = 'low_battery'
ATTR_MODEL_TYPE = 'model_type'
ATTR_GROUP_TYPE = 'group_type'
ATTR_DEVICE_RSSI = 'device_rssi'
ATTR_DUTY_CYCLE = 'duty_cycle'
ATTR_CONNECTED = 'connected'
ATTR_SABOTAGE = 'sabotage'
ATTR_OPERATION_LOCK = 'operation_lock'
class HomematicipGenericDevice(Entity):
"""Representation of an HomematicIP generic device."""
def __init__(self, home, device, post=None):
"""Initialize the generic device."""
self._home = home
self._device = device
self.post = post
_LOGGER.info('Setting up %s (%s)', self.name,
self._device.modelType)
async def async_added_to_hass(self):
"""Register callbacks."""
self._device.on_update(self._device_changed)
def _device_changed(self, json, **kwargs):
"""Handle device state changes."""
_LOGGER.debug('Event %s (%s)', self.name, self._device.modelType)
self.async_schedule_update_ha_state()
@property
def name(self):
"""Return the name of the generic device."""
name = self._device.label
if (self._home.name is not None and self._home.name != ''):
name = "{} {}".format(self._home.name, name)
if (self.post is not None and self.post != ''):
name = "{} {}".format(name, self.post)
return name
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def available(self):
"""Device available."""
return not self._device.unreach
@property
def device_state_attributes(self):
"""Return the state attributes of the generic device."""
return {
ATTR_LOW_BATTERY: self._device.lowBat,
ATTR_MODEL_TYPE: self._device.modelType
}

View File

@@ -0,0 +1,22 @@
"""Errors for the HomematicIP component."""
from homeassistant.exceptions import HomeAssistantError
class HmipcException(HomeAssistantError):
"""Base class for HomematicIP exceptions."""
class HmipcConnectionError(HmipcException):
"""Unable to connect to the HomematicIP cloud server."""
class HmipcConnectionWait(HmipcException):
"""Wait for registration to the HomematicIP cloud server."""
class HmipcRegistrationFailed(HmipcException):
"""Registration on HomematicIP cloud failed."""
class HmipcPressButton(HmipcException):
"""User needs to press the blue button."""

View File

@@ -0,0 +1,256 @@
"""Accesspoint for the HomematicIP Cloud component."""
import asyncio
import logging
from homeassistant import config_entries
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.core import callback
from .const import (
HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_PIN, HMIPC_NAME,
COMPONENTS)
from .errors import HmipcConnectionError
_LOGGER = logging.getLogger(__name__)
class HomematicipAuth(object):
"""Manages HomematicIP client registration."""
def __init__(self, hass, config):
"""Initialize HomematicIP Cloud client registration."""
self.hass = hass
self.config = config
self.auth = None
async def async_setup(self):
"""Connect to HomematicIP for registration."""
try:
self.auth = await self.get_auth(
self.hass,
self.config.get(HMIPC_HAPID),
self.config.get(HMIPC_PIN)
)
return True
except HmipcConnectionError:
return False
async def async_checkbutton(self):
"""Check blue butten has been pressed."""
from homematicip.base.base_connection import HmipConnectionError
try:
await self.auth.isRequestAcknowledged()
return True
except HmipConnectionError:
return False
async def async_register(self):
"""Register client at HomematicIP."""
from homematicip.base.base_connection import HmipConnectionError
try:
authtoken = await self.auth.requestAuthToken()
await self.auth.confirmAuthToken(authtoken)
return authtoken
except HmipConnectionError:
return False
async def get_auth(self, hass, hapid, pin):
"""Create a HomematicIP access point object."""
from homematicip.aio.auth import AsyncAuth
from homematicip.base.base_connection import HmipConnectionError
auth = AsyncAuth(hass.loop, async_get_clientsession(hass))
print(auth)
try:
await auth.init(hapid)
if pin:
auth.pin = pin
await auth.connectionRequest('HomeAssistant')
except HmipConnectionError:
return False
return auth
class HomematicipHAP(object):
"""Manages HomematicIP http and websocket connection."""
def __init__(self, hass, config_entry):
"""Initialize HomematicIP cloud connection."""
self.hass = hass
self.config_entry = config_entry
self.home = None
self._ws_close_requested = False
self._retry_task = None
self._tries = 0
self._accesspoint_connected = True
self._retry_setup = None
async def async_setup(self, tries=0):
"""Initialize connection."""
try:
self.home = await self.get_hap(
self.hass,
self.config_entry.data.get(HMIPC_HAPID),
self.config_entry.data.get(HMIPC_AUTHTOKEN),
self.config_entry.data.get(HMIPC_NAME)
)
except HmipcConnectionError:
retry_delay = 2 ** min(tries + 1, 6)
_LOGGER.error("Error connecting to HomematicIP with HAP %s. "
"Retrying in %d seconds.",
self.config_entry.data.get(HMIPC_HAPID), retry_delay)
async def retry_setup(_now):
"""Retry setup."""
if await self.async_setup(tries + 1):
self.config_entry.state = config_entries.ENTRY_STATE_LOADED
self._retry_setup = self.hass.helpers.event.async_call_later(
retry_delay, retry_setup)
return False
_LOGGER.info('Connected to HomematicIP with HAP %s.',
self.config_entry.data.get(HMIPC_HAPID))
for component in COMPONENTS:
self.hass.async_add_job(
self.hass.config_entries.async_forward_entry_setup(
self.config_entry, component)
)
return True
@callback
def async_update(self, *args, **kwargs):
"""Async update the home device.
Triggered when the hmip HOME_CHANGED event has fired.
There are several occasions for this event to happen.
We are only interested to check whether the access point
is still connected. If not, device state changes cannot
be forwarded to hass. So if access point is disconnected all devices
are set to unavailable.
"""
if not self.home.connected:
_LOGGER.error(
"HMIP access point has lost connection with the cloud")
self._accesspoint_connected = False
self.set_all_to_unavailable()
elif not self._accesspoint_connected:
# Explicitly getting an update as device states might have
# changed during access point disconnect."""
job = self.hass.async_add_job(self.get_state())
job.add_done_callback(self.get_state_finished)
async def get_state(self):
"""Update hmip state and tell hass."""
await self.home.get_current_state()
self.update_all()
def get_state_finished(self, future):
"""Execute when get_state coroutine has finished."""
from homematicip.base.base_connection import HmipConnectionError
try:
future.result()
except HmipConnectionError:
# Somehow connection could not recover. Will disconnect and
# so reconnect loop is taking over.
_LOGGER.error(
"updating state after himp access point reconnect failed.")
self.hass.async_add_job(self.home.disable_events())
def set_all_to_unavailable(self):
"""Set all devices to unavailable and tell Hass."""
for device in self.home.devices:
device.unreach = True
self.update_all()
def update_all(self):
"""Signal all devices to update their state."""
for device in self.home.devices:
device.fire_update_event()
async def _handle_connection(self):
"""Handle websocket connection."""
from homematicip.base.base_connection import HmipConnectionError
try:
await self.home.get_current_state()
except HmipConnectionError:
return
hmip_events = await self.home.enable_events()
try:
await hmip_events
except HmipConnectionError:
return
async def async_connect(self):
"""Start websocket connection."""
from homematicip.base.base_connection import HmipConnectionError
tries = 0
while True:
try:
await self.home.get_current_state()
hmip_events = await self.home.enable_events()
tries = 0
await hmip_events
except HmipConnectionError:
pass
if self._ws_close_requested:
break
self._ws_close_requested = False
tries += 1
retry_delay = 2 ** min(tries + 1, 6)
_LOGGER.error("Error connecting to HomematicIP with HAP %s. "
"Retrying in %d seconds.",
self.config_entry.data.get(HMIPC_HAPID), retry_delay)
try:
self._retry_task = self.hass.async_add_job(asyncio.sleep(
retry_delay, loop=self.hass.loop))
await self._retry_task
except asyncio.CancelledError:
break
async def async_reset(self):
"""Close the websocket connection."""
self._ws_close_requested = True
if self._retry_setup is not None:
self._retry_setup.cancel()
if self._retry_task is not None:
self._retry_task.cancel()
self.home.disable_events()
_LOGGER.info("Closed connection to HomematicIP cloud server.")
for component in COMPONENTS:
await self.hass.config_entries.async_forward_entry_unload(
self.config_entry, component)
return True
async def get_hap(self, hass, hapid, authtoken, name):
"""Create a HomematicIP access point object."""
from homematicip.aio.home import AsyncHome
from homematicip.base.base_connection import HmipConnectionError
home = AsyncHome(hass.loop, async_get_clientsession(hass))
home.name = name
home.label = 'Access Point'
home.modelType = 'HmIP-HAP'
home.set_auth_token(authtoken)
try:
await home.init(hapid)
await home.get_current_state()
except HmipConnectionError:
raise HmipcConnectionError
home.on_update(self.async_update)
hass.loop.create_task(self.async_connect())
return home

View File

@@ -0,0 +1,30 @@
{
"config": {
"title": "HomematicIP Cloud",
"step": {
"init": {
"title": "Pick HomematicIP Accesspoint",
"data": {
"hapid": "Accesspoint ID (SGTIN)",
"pin": "Pin Code (optional)",
"name": "Name (optional, used as name prefix for all devices)"
}
},
"link": {
"title": "Link Accesspoint",
"description": "Press the blue button on the accesspoint and the submit button to register HomematicIP with Home Assistant.\n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)"
}
},
"error": {
"register_failed": "Failed to register, please try again.",
"invalid_pin": "Invalid PIN, please try again.",
"press_the_button": "Please press the blue button.",
"timeout_button": "Blue button press timeout, please try again."
},
"abort": {
"unknown": "Unknown error occurred.",
"conection_aborted": "Could not connect to HMIP server",
"already_configured": "Accesspoint is already configured"
}
}
}

View File

@@ -19,6 +19,7 @@ import homeassistant.helpers.config_validation as cv
import homeassistant.remote as rem
import homeassistant.util as hass_util
from homeassistant.util.logging import HideSensitiveDataFilter
from homeassistant.util import ssl as ssl_util
from .auth import setup_auth
from .ban import setup_bans
@@ -49,21 +50,6 @@ CONF_TRUSTED_NETWORKS = 'trusted_networks'
CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold'
CONF_IP_BAN_ENABLED = 'ip_ban_enabled'
# TLS configuration follows the best-practice guidelines specified here:
# https://wiki.mozilla.org/Security/Server_Side_TLS
# Modern guidelines are followed.
SSL_VERSION = ssl.PROTOCOL_TLS # pylint: disable=no-member
SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | \
ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | \
ssl.OP_CIPHER_SERVER_PREFERENCE
if hasattr(ssl, 'OP_NO_COMPRESSION'):
SSL_OPTS |= ssl.OP_NO_COMPRESSION
CIPHERS = "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \
"ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \
"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \
"ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:" \
"ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"
_LOGGER = logging.getLogger(__name__)
DEFAULT_SERVER_HOST = '0.0.0.0'
@@ -201,8 +187,7 @@ class HomeAssistantHTTP(object):
support_legacy=hass.auth.support_legacy,
api_password=api_password)
if cors_origins:
setup_cors(app, cors_origins)
setup_cors(app, cors_origins)
app['hass'] = hass
@@ -240,7 +225,7 @@ class HomeAssistantHTTP(object):
'{0} missing required attribute "name"'.format(class_name)
)
view.register(self.app.router)
view.register(self.app, self.app.router)
def register_redirect(self, url, redirect_to):
"""Register a redirect with the server.
@@ -300,9 +285,7 @@ class HomeAssistantHTTP(object):
if self.ssl_certificate:
try:
context = ssl.SSLContext(SSL_VERSION)
context.options |= SSL_OPTS
context.set_ciphers(CIPHERS)
context = ssl_util.server_context()
context.load_cert_chain(self.ssl_certificate, self.ssl_key)
except OSError as error:
_LOGGER.error("Could not read SSL certificate from %s: %s",

View File

@@ -27,7 +27,8 @@ def setup_auth(app, trusted_networks, use_auth,
if use_auth and (HTTP_HEADER_HA_AUTH in request.headers or
DATA_API_PASSWORD in request.query):
_LOGGER.warning('Please use access_token instead api_password.')
_LOGGER.warning('Please change to use bearer token access %s',
request.path)
legacy_auth = (not use_auth or support_legacy) and api_password
if (hdrs.AUTHORIZATION in request.headers and

View File

@@ -72,7 +72,11 @@ async def ban_middleware(request, handler):
async def process_wrong_login(request):
"""Process a wrong login attempt."""
"""Process a wrong login attempt.
Increase failed login attempts counter for remote IP address.
Add ip ban entry if failed login attempts exceeds threshold.
"""
remote_addr = request[KEY_REAL_IP]
msg = ('Login attempt or request with invalid authentication '
@@ -107,7 +111,28 @@ async def process_wrong_login(request):
'Banning IP address', NOTIFICATION_ID_BAN)
class IpBan(object):
async def process_success_login(request):
"""Process a success login attempt.
Reset failed login attempts counter for remote IP address.
No release IP address from banned list function, it can only be done by
manual modify ip bans config file.
"""
remote_addr = request[KEY_REAL_IP]
# Check if ban middleware is loaded
if (KEY_BANNED_IPS not in request.app or
request.app[KEY_LOGIN_THRESHOLD] < 1):
return
if remote_addr in request.app[KEY_FAILED_LOGIN_ATTEMPTS] and \
request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] > 0:
_LOGGER.debug('Login success, reset failed login attempts counter'
' from %s', remote_addr)
request.app[KEY_FAILED_LOGIN_ATTEMPTS].pop(remote_addr)
class IpBan:
"""Represents banned IP address."""
def __init__(self, ip_ban: str, banned_at: datetime = None) -> None:

View File

@@ -27,6 +27,20 @@ def setup_cors(app, origins):
) for host in origins
})
def allow_cors(route, methods):
"""Allow cors on a route."""
cors.add(route, {
'*': aiohttp_cors.ResourceOptions(
allow_headers=ALLOWED_CORS_HEADERS,
allow_methods=methods,
)
})
app['allow_cors'] = allow_cors
if not origins:
return
async def cors_startup(app):
"""Initialize cors when app starts up."""
cors_added = set()

View File

@@ -12,6 +12,7 @@ from aiohttp import web
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPInternalServerError
import homeassistant.remote as rem
from homeassistant.components.http.ban import process_success_login
from homeassistant.core import is_callback
from homeassistant.const import CONTENT_TYPE_JSON
@@ -26,7 +27,9 @@ class HomeAssistantView(object):
url = None
extra_urls = []
requires_auth = True # Views inheriting from this class can override this
# Views inheriting from this class can override this
requires_auth = True
cors_allowed = False
# pylint: disable=no-self-use
def json(self, result, status_code=200, headers=None):
@@ -51,10 +54,11 @@ class HomeAssistantView(object):
data['code'] = message_code
return self.json(data, status_code, headers=headers)
def register(self, router):
def register(self, app, router):
"""Register the view with a router."""
assert self.url is not None, 'No url set for view'
urls = [self.url] + self.extra_urls
routes = []
for method in ('get', 'post', 'delete', 'put'):
handler = getattr(self, method, None)
@@ -65,13 +69,15 @@ class HomeAssistantView(object):
handler = request_handler_factory(self, handler)
for url in urls:
router.add_route(method, url, handler)
routes.append(
(method, router.add_route(method, url, handler))
)
# aiohttp_cors does not work with class based views
# self.app.router.add_route('*', self.url, self, name=self.name)
if not self.cors_allowed:
return
# for url in self.extra_urls:
# self.app.router.add_route('*', url, self)
for method, route in routes:
app['allow_cors'](route, [method.upper()])
def request_handler_factory(view, handler):
@@ -86,8 +92,11 @@ def request_handler_factory(view, handler):
authenticated = request.get(KEY_AUTHENTICATED, False)
if view.requires_auth and not authenticated:
raise HTTPUnauthorized()
if view.requires_auth:
if authenticated:
await process_success_login(request)
else:
raise HTTPUnauthorized()
_LOGGER.info('Serving %s to %s (auth: %s)',
request.path, request.get(KEY_REAL_IP), authenticated)

View File

@@ -69,27 +69,32 @@ SERVICE_SCAN_SCHEMA = vol.Schema({
@bind_hass
def scan(hass, entity_id=None):
"""Force process an image."""
"""Force process of all cameras or given entity."""
hass.add_job(async_scan, hass, entity_id)
@callback
@bind_hass
def async_scan(hass, entity_id=None):
"""Force process of all cameras or given entity."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_SCAN, data)
hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SCAN, data))
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Set up the image processing."""
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
yield from component.async_setup(config)
await component.async_setup(config)
@asyncio.coroutine
def async_scan_service(service):
async def async_scan_service(service):
"""Service handler for scan."""
image_entities = component.async_extract_from_service(service)
update_task = [entity.async_update_ha_state(True) for
entity in image_entities]
if update_task:
yield from asyncio.wait(update_task, loop=hass.loop)
await asyncio.wait(update_task, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_SCAN, async_scan_service,
@@ -124,8 +129,7 @@ class ImageProcessingEntity(Entity):
"""
return self.hass.async_add_job(self.process_image, image)
@asyncio.coroutine
def async_update(self):
async def async_update(self):
"""Update image and process it.
This method is a coroutine.
@@ -134,7 +138,7 @@ class ImageProcessingEntity(Entity):
image = None
try:
image = yield from camera.async_get_image(
image = await camera.async_get_image(
self.camera_entity, timeout=self.timeout)
except HomeAssistantError as err:
@@ -142,7 +146,7 @@ class ImageProcessingEntity(Entity):
return
# process image data
yield from self.async_process_image(image.content)
await self.async_process_image(image.content)
class ImageProcessingFaceEntity(ImageProcessingEntity):

View File

@@ -10,20 +10,26 @@ import logging
import requests
import voluptuous as vol
from homeassistant.const import ATTR_NAME
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_NAME)
from homeassistant.core import split_entity_id
import homeassistant.helpers.config_validation as cv
from homeassistant.components.image_processing import (
PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_CONFIDENCE, CONF_SOURCE,
CONF_ENTITY_ID, CONF_NAME)
CONF_ENTITY_ID, CONF_NAME, DOMAIN)
from homeassistant.const import (CONF_IP_ADDRESS, CONF_PORT)
_LOGGER = logging.getLogger(__name__)
ATTR_BOUNDING_BOX = 'bounding_box'
ATTR_CLASSIFIER = 'classifier'
ATTR_IMAGE_ID = 'image_id'
ATTR_MATCHED = 'matched'
CLASSIFIER = 'facebox'
DATA_FACEBOX = 'facebox_classifiers'
EVENT_CLASSIFIER_TEACH = 'image_processing.teach_classifier'
FILE_PATH = 'file_path'
SERVICE_TEACH_FACE = 'facebox_teach_face'
TIMEOUT = 9
@@ -32,6 +38,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PORT): cv.port,
})
SERVICE_TEACH_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_NAME): cv.string,
vol.Required(FILE_PATH): cv.string,
})
def encode_image(image):
"""base64 encode an image stream."""
@@ -63,18 +75,65 @@ def parse_faces(api_faces):
return known_faces
def post_image(url, image):
"""Post an image to the classifier."""
try:
response = requests.post(
url,
json={"base64": encode_image(image)},
timeout=TIMEOUT
)
return response
except requests.exceptions.ConnectionError:
_LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER)
def valid_file_path(file_path):
"""Check that a file_path points to a valid file."""
try:
cv.isfile(file_path)
return True
except vol.Invalid:
_LOGGER.error(
"%s error: Invalid file path: %s", CLASSIFIER, file_path)
return False
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the classifier."""
if DATA_FACEBOX not in hass.data:
hass.data[DATA_FACEBOX] = []
entities = []
for camera in config[CONF_SOURCE]:
entities.append(FaceClassifyEntity(
facebox = FaceClassifyEntity(
config[CONF_IP_ADDRESS],
config[CONF_PORT],
camera[CONF_ENTITY_ID],
camera.get(CONF_NAME)
))
camera.get(CONF_NAME))
entities.append(facebox)
hass.data[DATA_FACEBOX].append(facebox)
add_devices(entities)
def service_handle(service):
"""Handle for services."""
entity_ids = service.data.get('entity_id')
classifiers = hass.data[DATA_FACEBOX]
if entity_ids:
classifiers = [c for c in classifiers if c.entity_id in entity_ids]
for classifier in classifiers:
name = service.data.get(ATTR_NAME)
file_path = service.data.get(FILE_PATH)
classifier.teach(name, file_path)
hass.services.register(
DOMAIN,
SERVICE_TEACH_FACE,
service_handle,
schema=SERVICE_TEACH_SCHEMA)
class FaceClassifyEntity(ImageProcessingFaceEntity):
"""Perform a face classification."""
@@ -82,7 +141,8 @@ class FaceClassifyEntity(ImageProcessingFaceEntity):
def __init__(self, ip, port, camera_entity, name=None):
"""Init with the API key and model id."""
super().__init__()
self._url = "http://{}:{}/{}/check".format(ip, port, CLASSIFIER)
self._url_check = "http://{}:{}/{}/check".format(ip, port, CLASSIFIER)
self._url_teach = "http://{}:{}/{}/teach".format(ip, port, CLASSIFIER)
self._camera = camera_entity
if name:
self._name = name
@@ -94,28 +154,54 @@ class FaceClassifyEntity(ImageProcessingFaceEntity):
def process_image(self, image):
"""Process an image."""
response = {}
try:
response = requests.post(
self._url,
json={"base64": encode_image(image)},
timeout=TIMEOUT
).json()
except requests.exceptions.ConnectionError:
_LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER)
response['success'] = False
if response['success']:
total_faces = response['facesCount']
faces = parse_faces(response['faces'])
self._matched = get_matched_faces(faces)
self.process_faces(faces, total_faces)
response = post_image(self._url_check, image)
if response is not None:
response_json = response.json()
if response_json['success']:
total_faces = response_json['facesCount']
faces = parse_faces(response_json['faces'])
self._matched = get_matched_faces(faces)
self.process_faces(faces, total_faces)
else:
self.total_faces = None
self.faces = []
self._matched = {}
def teach(self, name, file_path):
"""Teach classifier a face name."""
if (not self.hass.config.is_allowed_path(file_path)
or not valid_file_path(file_path)):
return
with open(file_path, 'rb') as open_file:
response = requests.post(
self._url_teach,
data={ATTR_NAME: name, 'id': file_path},
files={'file': open_file})
if response.status_code == 200:
self.hass.bus.fire(
EVENT_CLASSIFIER_TEACH, {
ATTR_CLASSIFIER: CLASSIFIER,
ATTR_NAME: name,
FILE_PATH: file_path,
'success': True,
'message': None
})
elif response.status_code == 400:
_LOGGER.warning(
"%s teaching of file %s failed with message:%s",
CLASSIFIER, file_path, response.text)
self.hass.bus.fire(
EVENT_CLASSIFIER_TEACH, {
ATTR_CLASSIFIER: CLASSIFIER,
ATTR_NAME: name,
FILE_PATH: file_path,
'success': False,
'message': response.text
})
@property
def camera_entity(self):
"""Return camera entity id from process pictures."""
@@ -131,4 +217,5 @@ class FaceClassifyEntity(ImageProcessingFaceEntity):
"""Return the classifier attributes."""
return {
'matched_faces': self._matched,
'total_matched_faces': len(self._matched),
}

View File

@@ -6,3 +6,16 @@ scan:
entity_id:
description: Name(s) of entities to scan immediately.
example: 'image_processing.alpr_garage'
facebox_teach_face:
description: Teach facebox a face using a file.
fields:
entity_id:
description: The facebox entity to teach.
example: 'image_processing.facebox'
name:
description: The name of the face to teach.
example: 'my_name'
file_path:
description: The path to the image file.
example: '/images/my_image.jpg'

View File

@@ -174,7 +174,7 @@ class DeconzLight(Light):
data = {'on': False}
if ATTR_TRANSITION in kwargs:
data = {'bri': 0}
data['bri'] = 0
data['transitiontime'] = int(kwargs[ATTR_TRANSITION]) * 10
if ATTR_FLASH in kwargs:

View File

@@ -36,7 +36,6 @@ class EufyLight(Light):
def __init__(self, device):
"""Initialize the light."""
# pylint: disable=import-error
import lakeside
self._temp = None

View File

@@ -218,6 +218,9 @@ class FluxLight(Light):
def turn_on(self, **kwargs):
"""Turn the specified or all lights on."""
if not self.is_on:
self._bulb.turnOn()
hs_color = kwargs.get(ATTR_HS_COLOR)
if hs_color:
@@ -269,9 +272,6 @@ class FluxLight(Light):
else:
self._bulb.setRgb(*tuple(rgb), brightness=brightness)
if not self.is_on:
self._bulb.turnOn()
def turn_off(self, **kwargs):
"""Turn the specified or all lights off."""
self._bulb.turnOff()

View File

@@ -7,33 +7,39 @@ https://home-assistant.io/components/light.homematicip_cloud/
import logging
from homeassistant.components.light import Light
from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS)
from homeassistant.components.homematicip_cloud import (
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
ATTR_HOME_ID)
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
HMIPC_HAPID)
DEPENDENCIES = ['homematicip_cloud']
_LOGGER = logging.getLogger(__name__)
ATTR_POWER_CONSUMPTION = 'power_consumption'
ATTR_ENERGIE_COUNTER = 'energie_counter'
ATTR_ENERGIE_COUNTER = 'energie_counter_kwh'
ATTR_PROFILE_MODE = 'profile_mode'
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the HomematicIP light devices."""
from homematicip.device import (
BrandSwitchMeasuring)
"""Old way of setting up HomematicIP lights."""
pass
if discovery_info is None:
return
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up the HomematicIP lights from a config entry."""
from homematicip.aio.device import (
AsyncBrandSwitchMeasuring, AsyncDimmer)
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
devices = []
for device in home.devices:
if isinstance(device, BrandSwitchMeasuring):
if isinstance(device, AsyncBrandSwitchMeasuring):
devices.append(HomematicipLightMeasuring(home, device))
elif isinstance(device, AsyncDimmer):
devices.append(HomematicipDimmer(home, device))
if devices:
async_add_devices(devices)
@@ -64,13 +70,50 @@ class HomematicipLightMeasuring(HomematicipLight):
"""MomematicIP measuring light device."""
@property
def current_power_w(self):
"""Return the current power usage in W."""
return self._device.currentPowerConsumption
def device_state_attributes(self):
"""Return the state attributes of the generic device."""
attr = super().device_state_attributes
if self._device.currentPowerConsumption > 0.05:
attr.update({
ATTR_POWER_CONSUMPTION:
round(self._device.currentPowerConsumption, 2)
})
attr.update({
ATTR_ENERGIE_COUNTER: round(self._device.energyCounter, 2)
})
return attr
class HomematicipDimmer(HomematicipGenericDevice, Light):
"""MomematicIP dimmer light device."""
def __init__(self, home, device):
"""Initialize the dimmer light device."""
super().__init__(home, device)
@property
def today_energy_kwh(self):
"""Return the today total energy usage in kWh."""
if self._device.energyCounter is None:
return 0
return round(self._device.energyCounter)
def is_on(self):
"""Return true if device is on."""
return self._device.dimLevel != 0
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return int(self._device.dimLevel*255)
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_BRIGHTNESS
async def async_turn_on(self, **kwargs):
"""Turn the light on."""
if ATTR_BRIGHTNESS in kwargs:
await self._device.set_dim_level(
kwargs[ATTR_BRIGHTNESS]/255.0)
else:
await self._device.set_dim_level(1)
async def async_turn_off(self, **kwargs):
"""Turn the light off."""
await self._device.set_dim_level(0)

View File

@@ -21,7 +21,7 @@ from homeassistant.util.color import (
color_temperature_mired_to_kelvin, color_hs_to_RGB)
from homeassistant.helpers.restore_state import async_get_last_state
REQUIREMENTS = ['limitlessled==1.1.0']
REQUIREMENTS = ['limitlessled==1.1.2']
_LOGGER = logging.getLogger(__name__)
@@ -46,7 +46,7 @@ MIN_SATURATION = 10
WHITE = [0, 0]
SUPPORT_LIMITLESSLED_WHITE = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP |
SUPPORT_TRANSITION)
SUPPORT_EFFECT | SUPPORT_TRANSITION)
SUPPORT_LIMITLESSLED_DIMMER = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION)
SUPPORT_LIMITLESSLED_RGB = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT |
SUPPORT_FLASH | SUPPORT_COLOR |
@@ -239,6 +239,8 @@ class LimitlessLEDGroup(Light):
@property
def color_temp(self):
"""Return the temperature property."""
if self.hs_color is not None:
return None
return self._temperature
@property
@@ -247,6 +249,9 @@ class LimitlessLEDGroup(Light):
if self._effect == EFFECT_NIGHT:
return None
if self._color is None or self._color[1] == 0:
return None
return self._color
@property

View File

@@ -155,7 +155,11 @@ class MyStromLight(Light):
self._state = self._bulb.get_status()
colors = self._bulb.get_color()['color']
color_h, color_s, color_v = colors.split(';')
try:
color_h, color_s, color_v = colors.split(';')
except ValueError:
color_s, color_v = colors.split(';')
color_h = 0
self._color_h = int(color_h)
self._color_s = int(color_s)

View File

@@ -66,6 +66,8 @@ class TPLinkSmartBulb(Light):
self._brightness = None
self._hs = None
self._supported_features = 0
self._min_mireds = None
self._max_mireds = None
self._emeter_params = {}
@property
@@ -107,12 +109,12 @@ class TPLinkSmartBulb(Light):
@property
def min_mireds(self):
"""Return minimum supported color temperature."""
return kelvin_to_mired(self.smartbulb.valid_temperature_range[1])
return self._min_mireds
@property
def max_mireds(self):
"""Return maximum supported color temperature."""
return kelvin_to_mired(self.smartbulb.valid_temperature_range[0])
return self._max_mireds
@property
def color_temp(self):
@@ -195,5 +197,9 @@ class TPLinkSmartBulb(Light):
self._supported_features += SUPPORT_BRIGHTNESS
if self.smartbulb.is_variable_color_temp:
self._supported_features += SUPPORT_COLOR_TEMP
self._min_mireds = kelvin_to_mired(
self.smartbulb.valid_temperature_range[1])
self._max_mireds = kelvin_to_mired(
self.smartbulb.valid_temperature_range[0])
if self.smartbulb.is_color:
self._supported_features += SUPPORT_COLOR

View File

@@ -0,0 +1,102 @@
"""
Support for the Tuya light.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.tuya/
"""
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ENTITY_ID_FORMAT,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, Light)
from homeassistant.components.tuya import DATA_TUYA, TuyaDevice
from homeassistant.util import color as colorutil
DEPENDENCIES = ['tuya']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Tuya light platform."""
if discovery_info is None:
return
tuya = hass.data[DATA_TUYA]
dev_ids = discovery_info.get('dev_ids')
devices = []
for dev_id in dev_ids:
device = tuya.get_device_by_id(dev_id)
if device is None:
continue
devices.append(TuyaLight(device))
add_devices(devices)
class TuyaLight(TuyaDevice, Light):
"""Tuya light device."""
def __init__(self, tuya):
"""Init Tuya light device."""
super().__init__(tuya)
self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id())
@property
def brightness(self):
"""Return the brightness of the light."""
return self.tuya.brightness()
@property
def hs_color(self):
"""Return the hs_color of the light."""
return self.tuya.hs_color()
@property
def color_temp(self):
"""Return the color_temp of the light."""
color_temp = self.tuya.color_temp()
if color_temp is None:
return None
return colorutil.color_temperature_kelvin_to_mired(color_temp)
@property
def is_on(self):
"""Return true if light is on."""
return self.tuya.state()
@property
def min_mireds(self):
"""Return color temperature min mireds."""
return colorutil.color_temperature_kelvin_to_mired(
self.tuya.min_color_temp())
@property
def max_mireds(self):
"""Return color temperature max mireds."""
return colorutil.color_temperature_kelvin_to_mired(
self.tuya.max_color_temp())
def turn_on(self, **kwargs):
"""Turn on or control the light."""
if (ATTR_BRIGHTNESS not in kwargs
and ATTR_HS_COLOR not in kwargs
and ATTR_COLOR_TEMP not in kwargs):
self.tuya.turn_on()
if ATTR_BRIGHTNESS in kwargs:
self.tuya.set_brightness(kwargs[ATTR_BRIGHTNESS])
if ATTR_HS_COLOR in kwargs:
self.tuya.set_color(kwargs[ATTR_HS_COLOR])
if ATTR_COLOR_TEMP in kwargs:
color_temp = colorutil.color_temperature_mired_to_kelvin(
kwargs[ATTR_COLOR_TEMP])
self.tuya.set_color_temp(color_temp)
def turn_off(self, **kwargs):
"""Instruct the light to turn off."""
self.tuya.turn_off()
@property
def supported_features(self):
"""Flag supported features."""
supports = SUPPORT_BRIGHTNESS
if self.tuya.support_color():
supports = supports | SUPPORT_COLOR
if self.tuya.support_color_temp():
supports = supports | SUPPORT_COLOR_TEMP
return supports

View File

@@ -31,7 +31,7 @@ class XiaomiGatewayLight(XiaomiDevice, Light):
"""Initialize the XiaomiGatewayLight."""
self._data_key = 'rgb'
self._hs = (0, 0)
self._brightness = 180
self._brightness = 100
XiaomiDevice.__init__(self, device, name, xiaomi_hub)
@@ -64,7 +64,7 @@ class XiaomiGatewayLight(XiaomiDevice, Light):
brightness = rgba[0]
rgb = rgba[1:]
self._brightness = int(255 * brightness / 100)
self._brightness = brightness
self._hs = color_util.color_RGB_to_hs(*rgb)
self._state = True
return True
@@ -72,7 +72,7 @@ class XiaomiGatewayLight(XiaomiDevice, Light):
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._brightness
return int(255 * self._brightness / 100)
@property
def hs_color(self):

View File

@@ -310,7 +310,7 @@ class YeelightLight(Light):
bright = self._properties.get('bright', None)
if bright:
self._brightness = 255 * (int(bright) / 100)
self._brightness = round(255 * (int(bright) / 100))
temp_in_k = self._properties.get('ct', None)
if temp_in_k:

View File

@@ -324,9 +324,11 @@ class ZwaveColorLight(ZwaveDimmer):
else:
self._ct = TEMP_COLD_HASS
rgbw = '#00000000ff'
elif ATTR_HS_COLOR in kwargs:
self._hs = kwargs[ATTR_HS_COLOR]
if ATTR_WHITE_VALUE not in kwargs:
# white LED must be off in order for color to work
self._white = 0
if ATTR_WHITE_VALUE in kwargs or ATTR_HS_COLOR in kwargs:
rgbw = '#'

View File

@@ -83,7 +83,7 @@ def async_log_entry(hass, name, message, domain=None, entity_id=None):
hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data)
async def setup(hass, config):
async def async_setup(hass, config):
"""Listen for download events to download files."""
@callback
def log_message(service):

View File

@@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
SERVICE_PLAY_MEDIA)
from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['youtube_dl==2018.06.25']
REQUIREMENTS = ['youtube_dl==2018.07.04']
_LOGGER = logging.getLogger(__name__)

View File

@@ -12,19 +12,19 @@ import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_PAUSE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL,
MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_TURN_ON,
MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_SET, SUPPORT_PLAY)
SUPPORT_SELECT_SOURCE, SUPPORT_SELECT_SOUND_MODE,
SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL, MediaPlayerDevice,
PLATFORM_SCHEMA, SUPPORT_TURN_ON, MEDIA_TYPE_MUSIC,
SUPPORT_VOLUME_SET, SUPPORT_PLAY)
from homeassistant.const import (
CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED,
CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['denonavr==0.7.3']
REQUIREMENTS = ['denonavr==0.7.4']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = None
DEFAULT_SHOW_SOURCES = False
DEFAULT_TIMEOUT = 2
CONF_SHOW_ALL_SOURCES = 'show_all_sources'
@@ -33,6 +33,8 @@ CONF_VALID_ZONES = ['Zone2', 'Zone3']
CONF_INVALID_ZONES_ERR = 'Invalid Zone (expected Zone2 or Zone3)'
KEY_DENON_CACHE = 'denonavr_hosts'
ATTR_SOUND_MODE_RAW = 'sound_mode_raw'
SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
SUPPORT_SELECT_SOURCE | SUPPORT_VOLUME_SET
@@ -146,6 +148,20 @@ class DenonDevice(MediaPlayerDevice):
self._frequency = self._receiver.frequency
self._station = self._receiver.station
self._sound_mode_support = self._receiver.support_sound_mode
if self._sound_mode_support:
self._sound_mode = self._receiver.sound_mode
self._sound_mode_raw = self._receiver.sound_mode_raw
self._sound_mode_list = self._receiver.sound_mode_list
else:
self._sound_mode = None
self._sound_mode_raw = None
self._sound_mode_list = None
self._supported_features_base = SUPPORT_DENON
self._supported_features_base |= (self._sound_mode_support and
SUPPORT_SELECT_SOUND_MODE)
def update(self):
"""Get the latest status information from device."""
self._receiver.update()
@@ -163,6 +179,9 @@ class DenonDevice(MediaPlayerDevice):
self._band = self._receiver.band
self._frequency = self._receiver.frequency
self._station = self._receiver.station
if self._sound_mode_support:
self._sound_mode = self._receiver.sound_mode
self._sound_mode_raw = self._receiver.sound_mode_raw
@property
def name(self):
@@ -196,12 +215,22 @@ class DenonDevice(MediaPlayerDevice):
"""Return a list of available input sources."""
return self._source_list
@property
def sound_mode(self):
"""Return the current matched sound mode."""
return self._sound_mode
@property
def sound_mode_list(self):
"""Return a list of available sound modes."""
return self._sound_mode_list
@property
def supported_features(self):
"""Flag media player features that are supported."""
if self._current_source in self._receiver.netaudio_func_list:
return SUPPORT_DENON | SUPPORT_MEDIA_MODES
return SUPPORT_DENON
return self._supported_features_base | SUPPORT_MEDIA_MODES
return self._supported_features_base
@property
def media_content_id(self):
@@ -275,6 +304,15 @@ class DenonDevice(MediaPlayerDevice):
"""Episode of current playing media, TV show only."""
return None
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
attributes = {}
if (self._sound_mode_raw is not None and self._sound_mode_support and
self._power == 'ON'):
attributes[ATTR_SOUND_MODE_RAW] = self._sound_mode_raw
return attributes
def media_play_pause(self):
"""Simulate play pause media player."""
return self._receiver.toggle_play_pause()
@@ -291,6 +329,10 @@ class DenonDevice(MediaPlayerDevice):
"""Select input source."""
return self._receiver.set_input_func(source)
def select_sound_mode(self, sound_mode):
"""Select sound mode."""
return self._receiver.set_sound_mode(sound_mode)
def turn_on(self):
"""Turn on media player."""
if self._receiver.power_on():

View File

@@ -20,8 +20,7 @@ from homeassistant.const import (
STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN)
import homeassistant.util as util
REQUIREMENTS = ['https://github.com/wokar/pylgnetcast/archive/'
'v0.2.0.zip#pylgnetcast==0.2.0']
REQUIREMENTS = ['pylgnetcast-homeassistant==0.2.0.dev0']
_LOGGER = logging.getLogger(__name__)

View File

@@ -88,6 +88,8 @@ class LiveboxPlayTvDevice(MediaPlayerDevice):
import pyteleloisirs
try:
self._state = self.refresh_state()
# Update channel list
self.refresh_channel_list()
# Update current channel
channel = self._client.channel
if channel is not None:

View File

@@ -22,7 +22,7 @@ from homeassistant.const import (STATE_OFF, STATE_PAUSED, STATE_PLAYING,
STATE_IDLE)
from homeassistant import util
REQUIREMENTS = ['pexpect==4.0.1']
REQUIREMENTS = ['pexpect==4.6.0']
_LOGGER = logging.getLogger(__name__)
# SUPPORT_VOLUME_SET is close to available but we need volume up/down

View File

@@ -20,7 +20,7 @@ from homeassistant.const import (
from homeassistant.helpers.script import Script
from homeassistant.util import Throttle
REQUIREMENTS = ['ha-philipsjs==0.0.4']
REQUIREMENTS = ['ha-philipsjs==0.0.5']
_LOGGER = logging.getLogger(__name__)

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