Compare commits

...

351 Commits

Author SHA1 Message Date
Paulus Schoutsen
ae2153f1fc Merge pull request #22265 from home-assistant/rc
0.90.1
2019-03-21 14:24:28 -07:00
Pascal Vizeli
8085e9206a Update hass-nabucasa 0.10 (#22267) 2019-03-21 14:21:37 -07:00
Paulus Schoutsen
ff93591aaf Lint 2019-03-21 13:22:08 -07:00
Pascal Vizeli
8a314d7da0 Update Hass-NabuCasa 0.9 (#22258) 2019-03-21 13:06:15 -07:00
Andrew Sayre
f47a50aa24 Fix validate webhook requirements (#22248) 2019-03-21 13:06:13 -07:00
Michaël Arnauts
b2bb70b5aa Allow on/off on tado climate component. (#22242)
* Allow on/off on tado climate component. Bump python-tado version. Patch from @wmalgadey

* Revert wrongly change in tado device tracker
2019-03-21 13:05:46 -07:00
Jason Hunter
56cbe8a73f Stream fixes (#22238)
* fix issues with out of order packets, and empty first packet on some IP camera models

* do not skip the first packet
2019-03-21 13:05:09 -07:00
Paulus Schoutsen
0f730310a4 Bumped version to 0.90.1 2019-03-21 13:03:40 -07:00
Karim Roukoz
1aab284012 Bump total-connect-client to 0.25, fixing issue with Total Connect (#22230)
* Bump total-connect-client to 0.25

* Bump version in requirements_all.txt
2019-03-21 13:03:25 -07:00
Alexei Chetroi
a84ba90c9e Fix ZHA force polled entities. (#22222)
## Description:
Fix "force_polled" ZHA entities.

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

[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard/__init__.py#L14
[ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard/__init__.py#L23
2019-03-21 13:02:35 -07:00
Paulus Schoutsen
ee1c270c89 Updated frontend to 20190321.0 2019-03-21 12:57:47 -07:00
Paulus Schoutsen
937eba3dbe Merge pull request #22216 from home-assistant/rc
0.90.0
2019-03-20 10:16:24 -07:00
Paulus Schoutsen
9d8054e6e2 Bumped version to 0.90.0 2019-03-20 07:51:23 -07:00
Penny Wood
d4cd39e43e Fixed typing errors (#22207) 2019-03-20 07:51:14 -07:00
Paulus Schoutsen
1bf49ce5a3 Updated frontend to 20190320.0 2019-03-20 07:50:24 -07:00
Paulus Schoutsen
7cf1f4f9fe Bumped version to 0.90.0b7 2019-03-19 16:48:31 -07:00
Paulus Schoutsen
268d129ea9 Updated frontend to 20190319.1 2019-03-19 16:36:11 -07:00
Paulus Schoutsen
b8f246356a Bumped version to 0.90.0b6 2019-03-19 11:41:08 -07:00
Paulus Schoutsen
e6ffc790f2 Always load Hass.io component on Hass.io (#22185)
* Always load Hass.io component on Hass.io

* Lint

* Lint
2019-03-19 11:40:49 -07:00
Pascal Vizeli
b85189e699 Update Hass-NabuCasa 0.8 (#22177) 2019-03-19 11:40:48 -07:00
uchagani
f202114ead bump total_connect_client to 0.24 (#22166) 2019-03-19 11:40:06 -07:00
Paulus Schoutsen
fff6927f9c Updated frontend to 20190319.0 2019-03-19 11:38:16 -07:00
Paulus Schoutsen
ad0ec66353 Bumped version to 0.90.0b5 2019-03-18 17:04:49 -07:00
Franck Nijhof
592edd10ef Upgrade toonapilib to 3.2.2 + lower interval (#22160) 2019-03-18 17:04:43 -07:00
Pascal Vizeli
d75d75e49f Remove config check over supervisor (#22156)
* Remove config check over supervisor

* Fix lint

* Fix tests
2019-03-18 17:04:42 -07:00
Jason Hunter
1c9b750e36 Fix resetting access token on streams with keepalive (#22148) 2019-03-18 17:04:41 -07:00
WebSpider
33a7075883 Bump tado version (#22145)
* Bump python-tado, new API endpoint

* Change references of old API endpoint to new

* Update REQUIREMENTS
2019-03-18 17:04:40 -07:00
Paulus Schoutsen
cc00f3cd2e Allow non-admins to listen to certain events (#22137) 2019-03-18 17:04:39 -07:00
Jason Hu
22624715a9 Remove hass.config from aws_lambda notify payload (#22125) 2019-03-18 17:04:38 -07:00
Paulus Schoutsen
c37dcacf54 Updated frontend to 20190318.0 2019-03-18 16:54:31 -07:00
Paulus Schoutsen
872ee3eb21 Bumped version to 0.90.0b4 2019-03-16 23:26:48 -07:00
Jason Hunter
f218564185 delete previously removed service option from services yaml (#22123) 2019-03-16 23:26:17 -07:00
Paulus Schoutsen
16ac1d4600 Updated frontend to 20190316.0 2019-03-16 23:25:57 -07:00
Paulus Schoutsen
7b224dde23 Bumped version to 0.90.0b3 2019-03-15 23:20:19 -07:00
Jason Hunter
7a88c58ffa Beta Fix: FFMPEG and Stream component (#22091)
* remove stream_source from ffmpeg and onvif and add to generic ip cam

* fix tests
2019-03-15 23:19:52 -07:00
Paulus Schoutsen
68d1a5322a Prevent cloud remote UI when using 127.0.0.1 as trusted network (#22093)
* Prevent cloud remote UI when using trusted networks

* Limit to 127.0.0.1 trusted network

* Update error msg

* Disable ipv6 loopback
2019-03-15 23:19:26 -07:00
Jeff Irion
a46b64d227 Bump androidtv to 0.0.12 (#22072) 2019-03-15 23:19:26 -07:00
Paulus Schoutsen
5924479272 Updated frontend to 20190315.1 2019-03-15 23:19:09 -07:00
Paulus Schoutsen
aa81819683 Fix func 2019-03-15 11:12:12 -07:00
Paulus Schoutsen
ff6b86b5a8 Bumped version to 0.90.0b2 2019-03-15 10:59:55 -07:00
Paulus Schoutsen
3d404c43c8 Fix more test 2019-03-15 10:59:50 -07:00
Paulus Schoutsen
b18aef8d31 Fix test 2019-03-15 10:59:50 -07:00
Pascal Vizeli
ac1aeb35a6 Binary Sensor for Remote UI & Fix timezone (#22076)
* Binary Sensor for Remote UI

* Fix lint

* Revert make hass public

* Add tests
2019-03-15 10:59:27 -07:00
Jason Hu
3ec8b5a170 Correct context (#22061) 2019-03-15 10:59:26 -07:00
Paulus Schoutsen
8f10345468 Return config entry ID after creation (#22060) 2019-03-15 10:59:25 -07:00
Robbie Trencheny
0029dc3813 Mobile App: Expose Cloud Remote UI FQDN in registration response (#22055)
* Add a callback to get the cloud remote UI FQDN

* Expose Cloud Remote UI FQDN in the registration response

* Return a URL instead of FQDN
2019-03-15 10:59:25 -07:00
Robbie Trencheny
11ebb3f24e Mobile App: Discovery to default configuration.yaml, zeroconf to default_config (#22028)
* Move discovery into default configuration.yaml

* Add zeroconf to default_config
2019-03-15 10:59:25 -07:00
Robbie Trencheny
4835fb2c57 Mobile App: Enable loading via discovery (surprise inside!) (#22027)
![](http://funpeep.com/wp-content/uploads/2014/04/Cute-White-Cat-Wallpaper.jpg)
2019-03-15 10:59:23 -07:00
Jeff Irion
25a7f71ec2 Bump androidtv to 0.0.11 (#22025) 2019-03-15 10:59:22 -07:00
Robbie Trencheny
f0b7d76e26 Mobile App: Sensors (#21854)
## Description:

**Related issue (if applicable):** fixes #21782

## Checklist:
  - [x] The code change is tested and works locally.
  - [x] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
  - [x] There is no commented out code in this PR.
2019-03-15 10:57:24 -07:00
Robbie Trencheny
f7dcfe28b6 Mobile App: Register devices into the registry (#21856)
* Register devices into the registry

* Switch to device ID instead of webhook ID

* Rearchitect mobile_app to support config entries

* Kill DATA_REGISTRATIONS by migrating registrations into config entries

* Fix tests

* Improve how we get the config_entry_id

* Remove single_instance_allowed

* Simplify setup_registration

* Move webhook registering functions into __init__.py since they are only ever used once

* Kill get_registration websocket command

* Support description_placeholders in async_abort

* Add link to mobile_app implementing apps in abort dialog

* Store config entry and device registry entry in hass.data instead of looking it up

* Add testing to ensure that the config entry is created at registration

* Fix busted async_abort test

* Remove unnecessary check for entry is None
2019-03-15 10:57:00 -07:00
Robbie Trencheny
3fd1e8d382 Mobile App: Update Location schema updates & device ID generation (#21849)
* Update location schema

* Generate a random device ID at registration time for later use with device_tracker.see

* Remove host name from device_tracker.see payload

* Drop consider_home from the payload

* Remove stale consider_home in schema

* Remove source_type
2019-03-15 10:56:36 -07:00
Robbie Trencheny
c67113ad55 Mobile App: Support rendering multiple templates at once (#21851)
* Support rendering multiple templates at once

* Only catch TemplateError and dont log the error
2019-03-15 10:56:06 -07:00
Robbie Trencheny
b336322e9e Mobile App: Require encryption for registrations that support it (#21852)
## Description:

**Related issue (if applicable):** fixes #21758

## Checklist:
  - [x] The code change is tested and works locally.
  - [x] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
  - [x] There is no commented out code in this PR.
2019-03-15 10:55:45 -07:00
Paulus Schoutsen
9e6a7a6357 Updated frontend to 20190315.0 2019-03-15 10:49:13 -07:00
Paulus Schoutsen
fb895bba80 Bumped version to 0.90.0b1 2019-03-14 16:25:00 -07:00
Pascal Vizeli
707d32495b Fix Google Assistant User with Cloud (#22042)
* Fix Google Assistant User with Cloud

* Fix User Agent ID

* respell

* Fix object

* Fix tests

* fix lint

* Fix lint
2019-03-14 16:19:19 -07:00
Jason Hu
7057958e3e Fix lifx light async error (#22031) 2019-03-14 16:19:19 -07:00
emontnemery
90c878a7ed Update additional platforms to use new MQTT message callback (#22030)
* Move additional platforms to new MQTT callback

* Fix automation.mqtt
2019-03-14 16:19:18 -07:00
cgtobi
c78e332df3 Bring back the boiler status (#22021) 2019-03-14 16:19:18 -07:00
Phil Hawthorne
42c9472a74 Remove UTF8 decoding for Waze (#22020)
Removes the UFT8 decoding for the Waze sensor, which broke in 0.89

Fixes #21739
2019-03-14 16:19:17 -07:00
Paulus Schoutsen
07022c46f2 Version bump to 0.90.0b0 2019-03-13 13:03:58 -07:00
Paulus Schoutsen
b601fa52ba Bumped version to 0.89.0b0 2019-03-13 13:02:55 -07:00
Paulus Schoutsen
ce5efcdb26 Merge branch 'master' into dev 2019-03-13 13:02:26 -07:00
Gijs Reichert
1ffc0e3c7c Check updated_date for list and pick first (#22008) 2019-03-13 13:00:58 -07:00
Isabella Gross Alström
2dcd9d94c8 Allow all success status codes in REST notify response (#22011)
For example Discord webhooks returns a 204 success code as response, which gets logged as an error in the log, even though it is successful.
Update the allowed statuses to accept all 2xx responses as successful.
2019-03-13 13:00:08 -07:00
Andrew Sayre
83243e95d3 Remove default temp unit (#22012) 2019-03-13 12:59:37 -07:00
emontnemery
5957e4b75b Pass Message object to MQTT message callbacks (#21959)
* Pass Message object to MQTT message callbacks

* Improve method of detecting deprecated msg callback

* Fix mysensors

* Fixup

* Review comments

* Fix merge error
2019-03-13 12:58:20 -07:00
Paulus Schoutsen
50ec3d7de5 Update translations 2019-03-13 12:57:00 -07:00
Paulus Schoutsen
d0c8f6de56 Updated frontend to 20190313.0 2019-03-13 12:57:00 -07:00
Marco Orovecchia
cac8e34841 Nanoleaf availability check (#21945)
* Added availability check for nanoleaf lights

* pylint errors fixed

* pynanoleaf bump
2019-03-13 12:54:15 -07:00
Jc2k
deb66bb748 HomeKit controller light - remove code that can never execute (#21951) 2019-03-13 12:53:33 -07:00
beavis9k
eed1168fa1 fix error in LutronButton init if Button doesn't have a type (#21921) 2019-03-13 12:52:45 -07:00
Anders Melchiorsen
de2c7a9567 Wait for Sonos regrouping in service calls (#22006) 2019-03-13 12:51:41 -07:00
Anders Melchiorsen
fe5e4b5b9b Avoid playing queue pollution with Sonos unjoin (#22004) 2019-03-13 12:51:08 -07:00
Pascal Vizeli
186b48e2eb Bump NabuCasa library to 0.5 (#22010) 2019-03-13 16:45:26 +01:00
Aaron Bach
a71394a0ce Add program/zone enable/disable services to RainMachine (#21785) 2019-03-13 08:20:13 -06:00
Aaron Bach
897862fca4 Add availability and next run datetime to RainMachine switches (#21786) 2019-03-13 08:19:26 -06:00
endor
18daee9af6 Tellstick sensor configuration cleanup (#21402)
* Cleaned up named sensor handling for future

Breaking change for tellstick sensor configuration.

* Fixed linting of long lines and closing bracket

Linting warning for long line and not using best practice for visual indentation of closing bracket.
Who's a good boy..

* Whitespace on line was not cleaned.

* Removed spaces to clean up

* More.. whitespace. Sloppy.

* Constants from const, altered loops, added dictionary for sensor names.

* Fixed whitespace

* Inverted condition and created guard clause

* Changed condition from not ... in to not in.

* Fixed bad indentation on L91 and 92
2019-03-13 12:20:15 +01:00
Jeff Irion
007bf2bcb5 Rename 'firetv' to 'androidtv' and add Android TV functionality (#21944)
* Working on adding androidtv functionality to firetv component

* 'should_poll' must return True

* Change 'properties' to 'device_properties'

* Also mention 'Android TV' in services.yaml

* Use GitHub for 'androidtv' requirement

* Add 'androidtv==0.0.10' to requirements, remove 'firetv==1.0.9'

* Add 'GET_PROPERTIES' adb command option; use pypi for REQUIREMENTS

* Rename integration from 'firetv' to 'androidtv'

* Change default name to 'Android TV'

* Rename integration from 'firetv' to 'androidtv'

* Change firetv to androidtv in .coveragerc

* Change firetv to androidtv in requirements_all.txt

* Remove 'DEFAULT_APPS'
2019-03-13 11:18:59 +01:00
Jason Hu
e5da7a0014 Add breaking change section to PR template (#21994) 2019-03-13 10:54:30 +01:00
Anders Melchiorsen
c8692fe70c Use asyncio lock (#21985) 2019-03-13 10:17:09 +01:00
Daniel Perna
0162e2abe5 Update pyhomematic to 0.1.58 (#21989) 2019-03-13 08:56:59 +01:00
Paulus Schoutsen
c15f433c3e Add a service require_admin wrapper (#21953)
* Add a service require_admin wrapper

* Allow it to be used as a decorator

* Lint

* Add comment

* Add docstring

* Update syntax
2019-03-12 22:09:50 -07:00
Robbie Trencheny
bf839687ad Mobile App: Registration schema improvements (#21850)
* Update registration schema to add os_name (required) and make app_name required

* Ensure that a provided app_component is valid and available

* Ensure that component DEPENDENCIES declares mobile_app

* Update homeassistant/helpers/config_validation.py

* Standardize error responses

* Dont generalize REGISTER_BAD_COMPONENT

* Fix tests after merge
2019-03-12 22:04:27 -07:00
Diogo Gomes
a99d83390e Centrally define Energy Units (kWh and Wh) (#21719)
* centralize energy units kWh and Wh

* lint
2019-03-12 18:46:41 -07:00
Jc2k
c0b859d8da Set homekit controller entity as unavailable if new connections fail (#21901)
* Set entity as unavailable if new connections fail

* Fix docstring
2019-03-12 18:45:34 -07:00
Daniel Shokouhi
d66cc9befa Add stream source for amcrest component (#21983) 2019-03-12 21:40:24 -04:00
Jc2k
e618e2f348 Fix error introduced by #21933 (#21988) 2019-03-12 18:37:33 -07:00
Robbie Trencheny
9428ed7690 Use .get to ensure we dont get KeyError (#21993) 2019-03-12 18:00:18 -07:00
emontnemery
ce1fe06193 Write state directly in all MQTT platforms (#21971) 2019-03-12 14:46:48 -07:00
Paulus Schoutsen
f8921f84d7 skip flaky test (#21981) 2019-03-12 14:19:11 -07:00
Jc2k
2b1b47bfdd homekit_controller: Bump homekit to 0.13.0 (#21965)
* Bump homekit to 0.13.0

* Update gen_requirements_all.py

* Escape values used in TESTS_REQUIREMENTS
2019-03-12 21:54:08 +01:00
Paulus Schoutsen
97a87b2e4e Merge pull request #21982 from home-assistant/rc
0.89.2
2019-03-12 13:38:43 -07:00
Paulus Schoutsen
737c7e871d Bumped version to 0.89.2 2019-03-12 11:51:09 -07:00
Klaudiusz Staniek
1f3e4c5776 Fixes issues #21821 and #21819 (#21911)
* Fix #21821

* datetime fix

* local time to utc conversion fix

* Test cases update

* date import removed

* Update tod.py
2019-03-12 11:51:02 -07:00
Daniel Shokouhi
00d01865cf Fix botvac when no map exists (#21877) 2019-03-12 11:51:02 -07:00
Jason Hu
1b4905ae5a Override http.trusted_networks by auth_provider.trusted_networks (#21844) 2019-03-12 11:51:00 -07:00
Andrew Sayre
39749952ee Update dependencies to receive data on webhook callbacks (#21838) 2019-03-12 11:51:00 -07:00
Paulus Schoutsen
d3960bf745 Fix some cloud things (#21977) 2019-03-12 11:49:46 -07:00
Paulus Schoutsen
9416af5b56 Allow changing password without being admin (#21978) 2019-03-12 11:49:28 -07:00
Paulus Schoutsen
4a4bb43422 Stream: Only add base url when needed (#21979) 2019-03-12 11:46:20 -07:00
Paulus Schoutsen
d635111e4f Update translations 2019-03-12 11:31:17 -07:00
Paulus Schoutsen
9178ac17ad Updated frontend to 20190312.0 2019-03-12 11:30:50 -07:00
Sidney
1444a684e0 Fix MagicHome LEDs with flux_led component (#20733)
* bug fixing for MagicHome LEDs with flux_led component.

* corrections of the fixes for flux_led

* now asyncio sleep and turn on with brigthness possible

* indention fix with flux_led

* async now works

* houndci fixes

* little fixes for flux_led

* self._color fix for flux_led

* Add docstring
2019-03-12 16:51:51 +01:00
Paulus Schoutsen
d3bab30dbe Add cloud status (#21960)
* Add cloud status

* Expose certificate details

* store & reset last state

* Fix tests

* update tests

* update req

* fix lint
2019-03-12 15:54:04 +01:00
Markus Jankowski
ac97cebe11 Add Weather Sensors to Homematic IP (#21887)
* Add HmIP Weather Sensor Devices

* Fix test and icons

* fix test

* Fix comments
2019-03-12 14:52:13 +01:00
Thom Troy
cc34ee5559 fix ephember doing http call from property (#21855) 2019-03-12 14:49:36 +01:00
kbickar
62df6cbd09 Update to sense component to fully be async (#21698)
* Update to sense component to fully be async

* Shortened lines

* Whitespace
2019-03-12 14:44:53 +01:00
Anders Melchiorsen
dd11f8d3fe Avoid playing queue pollution when restoring Sonos snapshots (#21963)
Assume a snapshot state with three speakers in two groups, AB and C. They will
be playing the A and C queues, respectively. The B queue exists but is hidden
in this topology.

Unjoin B and form a new group BC, playing the B queue (now with the C queue
hidden).

To restore the snapshot we would join B back to A. The BC group would now only
contain the C speaker, still playing the B queue. The C queue has been lost :-(

The problem is that unjoining a coordinator will elect a new coordinator that
inherits the group queue and thus has its hidden queue overwritten.

This commit avoids the situation by having restore unjoin all slaves. Above, C
would be unjoined before joining B to A. This restores the C queue and since B
is then alone, it can be joined to A without having to transfer its playing
queue to remaining speakers.
2019-03-12 14:39:55 +01:00
Robbie Trencheny
2225425ed2 Update lametric icon to be HA logo (#21957) 2019-03-11 20:42:13 -07:00
Jason Hunter
7ccd0bba9a Live Streams Component (#21473)
* initial commit of streams

* refactor stream component

* refactor so stream formats are not considered a platform

* initial test and minor refactor

* fix linting

* update requirements

* need av in tests as well

* fix import in class def vs method

* fix travis and docker builds

* address code review comments

* fix logger, add stream start/stop logs, listen to HASS stop

* address additional code review comments

* beef up tests

* fix tests

* fix lint

* add stream_source to onvif camera

* address pr comments

* add keepalive to camera play_stream service

* remove keepalive and move import

* implement registry and have output provider remove itself from stream after idle, set libav log level to error
2019-03-11 19:57:10 -07:00
Klaudiusz Staniek
0a6ba14444 Fixes issues #21821 and #21819 (#21911)
* Fix #21821

* datetime fix

* local time to utc conversion fix

* Test cases update

* date import removed

* Update tod.py
2019-03-11 21:27:41 +01:00
Fabian Affolter
650658ea01 Upgrade schiene to 0.23 (#21940) 2019-03-11 21:25:29 +01:00
Pascal Vizeli
92ff49212b Offload Cloud component (#21937)
* Offload Cloud component & Remote support

* Make hound happy

* Address comments
2019-03-11 12:21:20 -07:00
Paulus Schoutsen
8bfbe3e085 Add update user command (#21922)
* Add update user command

* Add is_admin to current user
2019-03-11 12:08:02 -07:00
Jc2k
5e2302e469 Add an asyncio Lock around pairing, which cant be used concurrently (#21933) 2019-03-11 11:59:41 -07:00
Paulus Schoutsen
4f5446ff02 Add area permission check (#21835) 2019-03-11 11:02:37 -07:00
David McNett
4f49bdf262 Minor version bump for anthemav package (#21932)
Additional Python 3.7 fixes in the anthemav package.
See also: https://pypi.org/project/anthemav/
2019-03-11 20:47:57 +05:30
Marco Orovecchia
e7c85d350e Changed from nanoleaf_aurora to nanoleaf (#21913)
Nanoleaf component now supports both nanoleaf lights, Aurora and Canvas
Changed the dependency to pynanoleaf, nanoleaf does not seem to be
maintained anymore
2019-03-11 20:46:32 +05:30
Michaël Arnauts
bc76055c17 Allow inverting netdata sensor values (#21711)
* Allow inverting netdata sensor values

* Fix lint issue

* Use parentheses
2019-03-11 20:43:53 +05:30
Robbie Trencheny
785fd273e3 If registration supports encryption then return encrypted payloads (#21853) 2019-03-11 08:34:58 -04:00
Andrew Sayre
c401f35a43 Add cloudhook support to SmartThings component (#21905)
* Add support for Nabu Casa cloudhooks

* Added tests to cover cloudhook creation and removal

* Remove cloud dependency
2019-03-11 08:33:25 -04:00
gertdb
3fd6aa0ba9 Fixes Modbus service.yaml validity (#21923)
* Update __init__.py

* Update services.yaml

* Update services.yaml
2019-03-11 08:17:31 -04:00
Anders Melchiorsen
49014ac13f Remove confusing warning for TTS without entity_id (#21927) 2019-03-11 12:31:36 +01:00
Finbarr Brady
85dc5fe4d5 Update enigma2 based on review comments (#21890)
* Updates based on review comments

* bump netdisco and remove enigma2 init code

* revert netdisco bump
2019-03-11 11:42:56 +01:00
Jc2k
b0d55d1946 HomeKit controller config flow fixes (#21898)
* HomeKit controller config flow fixes

* This does work if you have latest vol-serialize
2019-03-11 10:07:12 +01:00
Brad Dixon
9ab543ab3d Add as_timestamp() to Jinja filters. (#21910)
Add as_timestamp() to Jinja filters
2019-03-11 13:21:22 +05:30
Colby Rome
29f01fb14e Add 'ssl' parameter for FiOS Quantum Gateway and upgrade Pypi (#21669)
* bump pypi version and take 'use_https' parameter

* changed to use CONF_SSL
2019-03-11 12:26:19 +08:00
Paulus Schoutsen
429bbc05dc Add WS subscription command for MQTT (#21696)
* Add WS subscription command for MQTT

* Add test

* Add check for connected

* Rename event_listeners to subscriptions
2019-03-10 20:07:09 -07:00
emontnemery
fc85b3fc5f Don't hang forever if manually added cast is down (#21565)
* Don't hang forever if manually added cast is down

* Adapt to pychromecast

* Do not set available until connected

* Update __init__.py

* Update requirements

* Lint, tests

* Fix tests
2019-03-10 19:57:30 -07:00
Jason Hu
fe1840f901 Deprecate http.api_password (#21884)
* Deprecated http.api_password

* Deprecated ApiConfig.api_password

GitHub Drafted PR would trigger CI after changed it to normal PR.
I have to commit a comment change to trigger it

* Trigger CI

* Adjust if- elif chain in auth middleware
2019-03-10 19:55:36 -07:00
Finbarr Brady
7ec7e51f70 bump netdisco to 2.4.0 (#21914) 2019-03-11 09:49:40 +08:00
Thiago Oliveira
77dc7595ee Allow emulated hue to set climate component temperature (#19034) 2019-03-11 00:04:21 +01:00
Niall Donegan
5debc8828a Return time based attributes as datetime in Unifi module (#21146)
* Return time based data as datetime in Unifi module

* Fix missing space and pylint complaints about import order.
2019-03-10 19:22:28 +01:00
Peter Epley
c888e65f11 Add custom holidays to workday sensor (#21718)
* Add custom holidays to workday sensor

* correcting copy/paste errors

* resolve block comment and add default

* resolve line too long

* fixed handling of no custom holidays

* hound fixes

* rerun Travis

* @fabaff requested changes

* spaces v tabs

* Fix validation
2019-03-10 22:44:45 +05:30
gertdb
bab53a1c94 Modbus write_register accept single value and array (#21621)
* Update __init__.py

* Update services.yaml
2019-03-10 20:38:29 +05:30
Thibault Maekelbergh
65ff8b727a Add myself to CODEOWNERS for NMBS code (#21892)
* Add myself to CODEOWNERS for NMBS code

* Fix
2019-03-10 20:17:17 +05:30
Thibault Maekelbergh
6456f71a46 Fix icon for sensor.discogs_random_record (#21891)
* Fix icon for sensor.discogs_random_record

* Add myself to CODEOWNERS for Discogs code
2019-03-10 19:19:31 +05:30
Marco M
05333f60d7 Fix missing code_required check in async_alarm_arm_night (#21858)
* Fix missing code_required check in async_alarm_arm_night

* Remove double code validation
Test added
2019-03-10 14:25:01 +01:00
Julius Mittenzwei
6f77d9bc34 Don't wait until final position of Velux cover is reached (#21558)
## Description:

* Bump version to latest version of pyvlx: 0.2.10. Library more failure tolerant, when detecting an unsupported device.
* When calling API (e.g. run scene, change position) don't wait until device has reached target position (This caused  HASS to be flaky while the device was moving)
* Support for vertical and horizontal awnings.
2019-03-10 03:47:22 -07:00
Joakim Sørensen
7102e82113 Change lib for whois sensor (#21878)
* Change lib for whois sensor

* Change requirements.txt
2019-03-10 14:38:13 +05:30
Paulus Schoutsen
ad73b6eee9 Updated frontend to 20190309.0 2019-03-09 21:26:35 -08:00
Paulus Schoutsen
5fbe2d5477 Update translations 2019-03-09 21:26:26 -08:00
David F. Mulcahey
5ffb471198 Update ZHA state handling (#21866)
* make device available if it was seen within 2 hours

* more state restore

* cleanup init

* clean up storage stuff

* fix tests

* update state handling
2019-03-09 20:09:09 -08:00
Paulus Schoutsen
5b2c6648fb Add user group (#21832)
* Add user group

* Rename system group to plural
2019-03-09 20:07:29 -08:00
Paulus Schoutsen
14b05b0a91 Fix incorrect 2nd param 2019-03-09 19:52:50 -08:00
Andrew Sayre
5ace55ea8d Add SmartThings sensor support for Three Axis (#21841)
* Add support for Three Axis to sensor platform

* Changes per review feedback.

* Remove unnecessary KeyError except

* Fix lint issue in line wrapping
2019-03-09 18:45:15 -06:00
Andrew Sayre
c927cd9c14 Add SmartThings climate support for Air Conditioners (#21840)
* Add support for Samsung Air Conditioners to Climate

* Changes per review feedback.

* Fix py3.5 compat list sorting in test
2019-03-09 18:43:16 -06:00
Daniel Shokouhi
2d2abc7831 Fix botvac when no map exists (#21877) 2019-03-09 23:06:35 +01:00
Zak
896075fa1c Add ClearPass Policy Manger device tracker (#21673)
* Adding ClearPass Policy Manger device tracker. Amending author

* Cleaned redundant code

* Updated .coveragerc

* Updated requirements_all.txt

* Implemented suggested changes partially.

* Implemented more suggested changes.

* Hound was unhappy

* Implement further suggested changes.

* Make Hound happy.

* Satisfy Travic CI

* Satisfy Travis CI #2

* Hound barking

* pylint else: return

* Implemented suggested changes minus AccessToken

* Removed access token logging

* Removed throttle import

* Removed period from debug string

* Make travis happy :(

* Moved source to new component structure.

* Forgot to rename source.
2019-03-09 22:12:29 +01:00
Paulus Schoutsen
f4f0d363ca Better cloud check (#21875) 2019-03-09 12:15:16 -08:00
David F. Mulcahey
ac5ccd651c Bump quirks for ZHA and handle resulting battery % change (#21869)
* bump quirks and handle battery change

* move inside guard

* round battery
2019-03-09 12:14:58 -08:00
Jason Hu
226be65910 Only commit if need. (#21848) 2019-03-09 12:04:13 -08:00
Paul Bottein
4d9cf15c45 Fix authorization header in cors (#21662)
* Fix authorization headers in cors

* Use aiohttp authorization header instead of custom const
2019-03-09 10:00:10 -08:00
emontnemery
fc81826763 Introduce Entity.async_write_ha_state() to not miss state transition (#21590)
* Copy state in schedule_update_ha_state

* Lint

* Fix broken test

* Review comment, improve docstring

* Preserve order of state updates

* Rewrite

* Break up async_update_ha_state

* Update binary_sensor.py

* Review comments

* Update docstring

* hass -> ha

* Update entity.py

* Update entity.py
2019-03-09 09:52:22 -08:00
Willem Burgers
458548daec Fix TypeError (#21734)
* timediff is of type timedelta. Divide by timedelta does not work.

- convert a timedelta to int
- make sure the test inputs real timestamps

* Convert the total_seconds to decimal and round the result

readings are of type Decimal, so fix test to reflect that

* split line into multiple statements

Line too long

* use total_seconds instead of timediff

* Make both values float instead of Decimal
2019-03-09 17:51:15 +01:00
Hackashaq666
be989ebb7e Update honeywell.py to read current humidity for US Thermostats (#21728)
* Update honeywell.py

Add thermostat humidity reading available in somecomfort for US thermostats.

* Update honeywell.py

* Update honeywell.py
2019-03-09 21:47:28 +05:30
Jason Hu
bbd01968ba Override http.trusted_networks by auth_provider.trusted_networks (#21844) 2019-03-08 23:56:37 -08:00
Robbie Trencheny
9ab0753cf7 mobile_app improvements (#21607)
* First webhook commands for getting and deleting single registrations

* Keep a list of deleted webhook IDs so we can 410 if the webhook receives traffic in the future

* Return a empty JSON object instead of None

* Split up mobile_app bits into individual files

* Add typing

* Sort keys

* Remove unused async_setup_entry

* New decorator method of registering webhooks

* Add tests for cloud hook forwarding and improve error handling for cloud hooks

* Initial implementation of platform specific logic

* Add get registrations by user ID websocket call, minor style fixes

* Stop using resp dictionary during registration

* Move mobile_app/ios.py to ios/mobile_app.py

* Log any errors encountered during webhook

* Improve update registration call

* Split up mobile_app tests to match split up component

* Fix tests

* Remove integration_map in favor of component name in registration

* Add a few helper functions for custom logic components to use

* Load the app_component platform at device registration or component setup time

* Remove extraneous function

* Use guard function for checking if component is in device

* Inline websocket schemas

* Rename ATTR_s used in storage to DATA_ prefix

* squash flake8 and pylint issues

* Remove ios.mobile_app platform

* Dont mark websocket_api as a dependency

* Return standard empty_okay_response with 400 if no JSON sent

* Ensure deleted webhook IDs are registered at launch

* Remove the creation of cloudhooks during handle_webhook

* Rename device to registration everywhere applicable

* Dont check if cloud is logged in, just check if cloud is in components

* Dont ever use cloudhook_id

* Remove component loading logic for a later PR

* Cast exception to string

* Remove unused functions
2019-03-08 23:44:56 -08:00
Aaron Bach
49eaa34e03 Fixed a misspelling in a docstring (#21846) 2019-03-08 22:36:41 -07:00
Andrew Sayre
c5734eecc7 Update dependencies to receive data on webhook callbacks (#21838) 2019-03-08 21:20:07 -08:00
Aaron Bach
113db9afd4 Fix config entry exception in Ambient PWS (#21836) 2019-03-08 21:25:35 -06:00
William Scanlon
012c657a9c Updated to pyeconet 0.0.10 (#21837) 2019-03-08 21:24:17 -06:00
engrbm87
76d11e4b74 fix empty TOPIC_BASE issue (#21740)
* fix empty TOPIC_BASE issue

if the value of the TOPIC_BASE is empty then we need to remove "~" from the topic value if it exists. 
by doing `if base:` on line 239 the condition will be false if the value is empty so the '~' will not be stripped from the topic value.
I simply removed the `if base:` line and added `if TOPIC_BASE in payload:`

* Update homeassistant/components/mqtt/discovery.py

Co-Authored-By: engrbm87 <engrbm87@gmail.com>
2019-03-08 16:48:54 -08:00
Mike Megally
d8ac761bb6 Synology sensor quick return if attr is null (#21709)
* Quick return if attr is null

There are some case where attr is null. Returning null doesn't change anything (in my case this is mapped to a volume that doesn't exist, not sure what others are seeing). 

If you have confirmed you hass instance for C instead of F you do not see this error.

* update == to is

* whitespace
2019-03-08 16:35:38 -08:00
Finbarr Brady
0f189809a9 Add support for Cisco Mobility Express (#21531)
* Move cisco me to new layout

* Add docstring

* Move items out of init method and pass the controller instance to the scanner in get_scanner

* Update homeassistant/components/cisco_mobility_express/device_tracker.py

Co-Authored-By: fbradyirl <fbradyirl@users.noreply.github.com>

* Update homeassistant/components/cisco_mobility_express/device_tracker.py

Co-Authored-By: fbradyirl <fbradyirl@users.noreply.github.com>

* Update homeassistant/components/cisco_mobility_express/device_tracker.py

Co-Authored-By: fbradyirl <fbradyirl@users.noreply.github.com>

* Update homeassistant/components/cisco_mobility_express/device_tracker.py

Co-Authored-By: fbradyirl <fbradyirl@users.noreply.github.com>

* Fix build error

* Cleanup based on comments.
2019-03-09 00:47:06 +01:00
Robbie Trencheny
31a4187cc0 Log if aiohttp hits error during IndieAuth (#21780)
* Log if aiohttp hits error during IndieAuth

* Add content of redirect_url into error log

Co-Authored-By: awarecan <awarecan@users.noreply.github.com>
2019-03-08 14:51:13 -08:00
Jason Hu
3d8673dbf8 Resolve auth_store loading race condition (#21794)
* Add lock in auth_store._async_load()

* Python 3.5 does not like assert_called_once()
2019-03-08 14:50:24 -08:00
Jason Hu
3ff2d99cd6 Load logger and system_log components as soon as possible (#21799) 2019-03-08 14:47:10 -08:00
Paulus Schoutsen
22ab5a498f Change how we import config modules (#21824) 2019-03-08 14:09:18 -08:00
uchagani
ed6082eb2b change paths to be relative (#21827) 2019-03-08 14:08:19 -08:00
Paulus Schoutsen
4c9e5eef9c Remove stub from config component (#21822) 2019-03-08 14:07:49 -08:00
Paulus Schoutsen
3da0ed9cc7 Onboarding to generate auth code (#21777) 2019-03-08 13:51:42 -08:00
Toon Willems
a0e8543aed remove occupancy, as it is not available at this level in the iRail api (#21810) 2019-03-09 02:49:48 +05:30
Greg Dowling
7226e917ed Bump loopenergy to 0.1.0. Loop updated their socket.io server from 0.9 to 2.0 - which required a library update. (#21809) 2019-03-09 02:13:59 +05:30
Finbarr Brady
4571f1bf0d Adding enigma2 media player (#21271)
* Updated based on review comments

* fix hound

* Update homeassistant/components/media_player/enigma2.py

* Update homeassistant/components/media_player/enigma2.py

* Update homeassistant/components/media_player/enigma2.py

* Update enigma2.py

* Update enigma2.py

* Move file and update docsstring

* Fix path in coverage rc file

* requirements

* Update media_player.py

* Setup discovery for e2

* Handle discovered devices

* Add reqs

* Update for auth for openwebif

* Forget to set DEFAULT_PASSWORD

* Add source selection

* Fix get current source name

* Update pip version

* - adding some extra attributes
- support better recording playback integration

* bump pip version

* Bump pip

* Adding prefer_picon config option

* Updates to move logic into pypi module

* bump pip

* bump pip

* - remove http dependancy.

- rename prefer_picon to use_channel_icon

* Bump pypi to fix toggle bug.

also fix travis

also move setup out of init
2019-03-08 14:56:10 +01:00
Ville Skyttä
dfd9f7ccf3 Upgrade huawei-lte-api to 1.1.5 (#21791) 2019-03-08 09:28:53 +01:00
Jose Motta Lopes
f705ac6b43 Add Time of Flight Sensor using VL53L1X (#21230)
* Add Time of Flight Sensor using VL53L1X

* Fix issues found by bot

* Fix issues from bot

* Remove extra logs

* Keep removing logs dependencies not used

* Remove log from update

* Add logger info to async_update

* Fix over-indented line

* Fix pylint error

* Remove logger reporting successful operation

* Update requirements

* Update requirements_all.txt

* Update requirements_test_all.txt

* Used isort to keep imports and added STMicroelectronics to docstring

* Replace time.sleep by asyncio.sleep

* Add requirements to COMMENT_REQUIREMENTS and fix typo

* Using async_add_executor_job to schedule the call in the pool

* Fix typo

* Optimize async_update

* Updated requirements files

* Group and schedule calls that should be run sequentially

* Fix lint errors

* Revision showing development history

* Cleaning and typos

* Cleaning and typos

* Fix wrong-import-order

* Fix gen_requirements_all

* Schedule rpi_gpio I/O on the executor thread pool

* Fix partial parameters

* Fix bot error - add blank line

* Fix lint error

* Remove dependencies from requirements

* Review initial commits

* Move all device I/O to async_update

* Update requirements_all.txt

* Revised header with no url to the docs

* Use async_added_to_hass to add and initialize the sensor

* Add docstring to init()

* Move sensor.open() to async_setup_platform

* Remove logging and async

* Fix typo

* Move sensor.open to safe initialization

* Fix typo

* Fix typo

* Add the new tof module to .coveragerc

* Move the sensor platform under a tof package

* Update .coveragerc and requirements_all for tof package
2019-03-08 08:21:22 +01:00
Paulus Schoutsen
ac4d5d7c30 Merge pull request #21778 from home-assistant/rc
0.89.1
2019-03-07 23:16:17 -08:00
Ville Skyttä
2812483193 Upgrade pylint to 2.3.1 (#21789) 2019-03-08 09:13:35 +02:00
Paulus Schoutsen
f3e8e34089 Add workflow for tests 2019-03-07 17:03:23 -08:00
Paulus Schoutsen
eae6d1c7a6 Bumped version to 0.89.1 2019-03-07 16:48:53 -08:00
William Scanlon
a121c92f52 Updated to newest pyeconet (#21772) 2019-03-07 16:48:47 -08:00
David Thulke
4d6f21ecb2 adds missing SUPPORT_VOLUME_SET flag to webos media_player (#21766) 2019-03-07 16:48:46 -08:00
Sebastian Muszynski
1638d0a92f Bump PyXiaomiGateway version to 0.12.2 (Closes: #21731) (#21764) 2019-03-07 16:48:46 -08:00
Jason Hu
c031fd4164 Fix script load module issue (#21763)
* Fix script load depedency

* Revert #21754
2019-03-07 16:48:45 -08:00
Jason Hu
78a806bc5c Fix script load module issue (#21763)
* Fix script load depedency

* Revert #21754
2019-03-07 16:48:14 -08:00
David Thulke
2fb978393b adds missing SUPPORT_VOLUME_SET flag to webos media_player (#21766) 2019-03-07 16:46:50 -08:00
William Scanlon
a843da1bfc Updated to newest pyeconet (#21772) 2019-03-07 16:46:01 -08:00
Sebastian Muszynski
cfd94ecbbc Bump PyXiaomiGateway version to 0.12.2 (Closes: #21731) (#21764) 2019-03-07 22:55:11 +01:00
Marvin Wichmann
5112f8f6b5 Introduce target_temperature_state_address for climate device (#21541) 2019-03-07 21:53:42 +00:00
Jason Hu
5f0c37ccfc Fix colorlog import error (#21754)
* Fix colorlog import error

* Lint
2019-03-07 11:07:24 -08:00
Jason Hu
c91fb82807 Fix colorlog import error (#21754)
* Fix colorlog import error

* Lint
2019-03-07 11:07:07 -08:00
Daniel Shokouhi
e412317194 Fix botvac connected maps call as it is not a supported model (#21752) 2019-03-07 11:06:42 -08:00
Leonardo Merza
44341a958a automated commit 07/03/2019 10:47:38 (#21749) 2019-03-07 11:06:41 -08:00
Markus Jankowski
5a555102b9 Fix group-switch availability for Homematic IP (#21640)
* Add available=True to groups

* Added unreach to stateattributes

* Fixed comments

* added missing sabotage check

* added missing lowBat check

* fix typo

* apply suggestion

Co-Authored-By: SukramJ <markus@mm-jankowski.de>

* apply suggestion

Co-Authored-By: SukramJ <markus@mm-jankowski.de>

* applied suggiestions

* readded lost str()

* fix comment
2019-03-07 11:06:40 -08:00
Markus Jankowski
aebe6ab70c Fix Name of Homematic IP accesspoint in devices, if name is configured (#21617)
* Fix Name of Accesspoint if name is configured

* fix lint

* Simplyfied naming

* applied suggestion

Co-Authored-By: SukramJ <markus@mm-jankowski.de>

* update comment
2019-03-07 11:06:39 -08:00
Kevin Fronczak
48c9758cf5 Upgrade blinkpy==0.13.1 (Fixes #21559) (#21578)
* Upgrade blinkpy with new api endpoint

* Change wifi units to dBm
2019-03-07 11:06:39 -08:00
Leonardo Merza
e356b48fca automated commit 07/03/2019 10:47:38 (#21749) 2019-03-07 11:03:32 -08:00
Daniel Shokouhi
fc943dc4b6 Fix botvac connected maps call as it is not a supported model (#21752) 2019-03-07 11:03:02 -08:00
Paulus Schoutsen
279470613c Updated frontend to 20190305.1 2019-03-07 10:54:56 -08:00
zewelor
f2abc91c1e Allow light toggle service to accept all turn on params (#20912) 2019-03-07 22:33:30 +05:30
Pascal Vizeli
61786b79f7 Revert pull request to push 2019-03-07 15:33:13 +01:00
Pascal Vizeli
720b0c5334 Revert Travis until github actions work better for PR (#21746) 2019-03-07 15:30:48 +01:00
Pascal Vizeli
5c2d174d5f Change github trigger type 2019-03-07 15:25:27 +01:00
Pascal Vizeli
02bcf46053 Update .travis.yml (#21736)
* Update .travis.yml

* Update tox.ini

* Update main.workflow

* Update tox.ini
2019-03-07 14:40:18 +01:00
Jason Hu
ba70459e1e Remove pytest warning message (#21713) 2019-03-07 16:59:15 +05:30
Markus Jankowski
1891d5bf22 Fix group-switch availability for Homematic IP (#21640)
* Add available=True to groups

* Added unreach to stateattributes

* Fixed comments

* added missing sabotage check

* added missing lowBat check

* fix typo

* apply suggestion

Co-Authored-By: SukramJ <markus@mm-jankowski.de>

* apply suggestion

Co-Authored-By: SukramJ <markus@mm-jankowski.de>

* applied suggiestions

* readded lost str()

* fix comment
2019-03-07 11:12:03 +01:00
Markus Jankowski
a46458d04f Fix Name of Homematic IP accesspoint in devices, if name is configured (#21617)
* Fix Name of Accesspoint if name is configured

* fix lint

* Simplyfied naming

* applied suggestion

Co-Authored-By: SukramJ <markus@mm-jankowski.de>

* update comment
2019-03-07 11:07:32 +01:00
Leonardo Merza
9c70b00403 tplink - catch SmartDeviceException on is_dimmable call (#21726)
* automated commit 06/03/2019 20:49:50

* automated commit 06/03/2019 20:53:13

* automated commit 06/03/2019 20:53:48
2019-03-07 10:54:09 +01:00
Jason Hu
38a93afa66 Make pytest in tox quite (#21727) 2019-03-06 22:27:46 -08:00
Jason Hu
a85119ac09 Fix pylint warning on python 3.7 (#21714) 2019-03-07 06:47:56 +01:00
c-soft
f4a9ad0b2e Fix initialization and add "pending" status of Satel integra (#21194)
* Added updating alarm state after start of the HA. Still rough and dirty.

* Fixed initialization of the panel and binary sensor.  Before cleanup.

* Added alarm clearing, linting fixes.

* Removed dead code, added style changes.

* Updated requirements

* Fixed linting errors.

* Fixed linting errors

* Fixed linter errors.

* Fixed hopefully last linter errors.

* Fixes after code review, imports sorted.

* Removed init debugging
2019-03-06 19:47:47 -08:00
Sergio Oller
9f06be750f Support multiple keys in ifttt triggers (#21454)
* Support multiple keys in ifttt triggers

* Rename `to` to `target` in ifttt.

Follow PR code review suggestions
2019-03-06 19:47:13 -08:00
Jc2k
dbf129dfdd Start preparing for homekit_controller config entries (#21564)
* Start preparing for homekit_controller config entries

* Review feedback

* Review feedback

* Only use the vol.strip validator for pairing_code

* CV not required now

* Changes from review

* Changes after review
2019-03-06 19:44:52 -08:00
Phil Bruckner
5616505032 Change amcrest camera_image to async (#21720)
Change AmcrestCam method camera_image to async so asyncio lock can be used instead of a threading lock. Bump amcrest package to 1.2.5.
2019-03-06 19:42:59 -08:00
Paulus Schoutsen
88bc3033d3 Merge pull request #21712 from home-assistant/rc
0.89.0
2019-03-06 15:15:37 -08:00
Paulus Schoutsen
21de636e5b Bumped version to 0.89.0 2019-03-06 10:07:31 -08:00
Franck Nijhof
87b5faa244 Upgrade toonapilib to 3.2.1 (#21706) 2019-03-06 10:05:32 -08:00
Fredrik Erlandsson
c2f4293c6a resync hass that changes have occured (#21705) 2019-03-06 10:05:31 -08:00
Franck Nijhof
0e36b26770 Upgrade toonapilib to 3.2.1 (#21706) 2019-03-06 07:40:29 -08:00
Diogo Gomes
8e9a496002 Utility Meter offset defined by a time_period (#20926)
* change offset from int to Time period dictionary

* track according to offset

* left overs... tks @fabaff

* typo
2019-03-06 07:55:24 -05:00
Fredrik Erlandsson
54895fcb1e resync hass that changes have occured (#21705) 2019-03-06 07:52:25 -05:00
Alan Tse
b7b034c532 Update to teslajsonpy v0.0.25 (#21702) 2019-03-05 21:44:37 -08:00
Penny Wood
d1038ea79f Google Assistant: Create and pass context to service calls (#21551)
* Google Assistant: Create and pass context to service calls

* Refactor request data into separate object and pass to execute.
2019-03-05 20:00:53 -08:00
Paulus Schoutsen
fc1ee9be43 Use new style for built-in ws commmands (#21694)
* Use new style for built-in ws commmands

* Lint
2019-03-05 19:31:26 -08:00
Paulus Schoutsen
c9b173405b Fix Z-Wave relative imports (#21693) 2019-03-05 17:17:58 -08:00
Paulus Schoutsen
4c72f3c48b Bumped version to 0.89.0b3 2019-03-05 11:46:30 -08:00
carstenschroeder
cb613984df Fix ADS race condition (#21677) 2019-03-05 11:46:19 -08:00
Diogo Gomes
4978a1681e check we have a tb (#21670) 2019-03-05 11:46:18 -08:00
Paulus Schoutsen
2303e1684e Updated frontend to 20190305.0 2019-03-05 11:45:46 -08:00
Paulus Schoutsen
467d8d616e Updated frontend to 20190305.0 2019-03-05 11:45:31 -08:00
Alan Tse
dbb92048aa Bump teslajsonpy to 0.0.24 (#21675)
* Bump teslajsonpy to 0.0.24

* Update requirements_all.txt
2019-03-05 09:23:00 -08:00
Jonathan McDowell
401720085d Allow 202 status code as a successful REST notify response (#21678)
The REST notification component only allows 200 + 201 as a successful
response code to the submission. notify.me returns a 202 (Accepted)
response, which works fine but gets logged as a warning in the log.
Update the allowed statuses to treat the 202 as ok.
2019-03-05 09:22:21 -08:00
Nick Whyte
7d9c14541b Bump nessclient version to 0.9.14 (#21679) 2019-03-05 09:21:13 -08:00
Phil Bruckner
16d79b52c3 Serialize amcrest snapshot commands and bump PyPI package to 1.2.4 (#21664)
* Serialize snapshot commands and bump amcrest package to 1.2.4

Attempting to send a snapshot command when a previous one hasn't finished will result in warnings and/or errors. This can happen when the camera picture is clicked on in the frontend, resulting in the thread that updates the thumbnail in the background every 10 seconds to sometimes collide with the thread that updates the large picture in the foreground quickly. An automation that calls the camera.snapshot service in yet another thread can make the situation worse. Fix by adding a thread lock to serialize snapshot commands. Also bump the amcrest package to 1.2.4 which fixes error handling in the command method and improves performance by reusing requests sessions.

* Update amcrest package to 1.2.4
2019-03-05 17:03:19 +01:00
Steven Looman
3ffff887d8 Adds option in UPnP component to override callback url (#21583)
* Add option to override callback url

* Upgrade to async_upnp_client==0.14.5

* Fix requirements_all.txt
2019-03-05 15:48:44 +00:00
carstenschroeder
17c3c14833 Fix ADS race condition (#21677) 2019-03-05 11:07:40 +01:00
Colby Rome
0e78054195 Xfinity Gateway device_tracker platform (#21026)
* initial commit

* updated .coveragerc, CODEOWNERS, generated requirements_all.txt

* fixed lines exceeding 79 characters

* pylint fixes

* shorten docstring and simplify get_scanner

* extract initialization into get_scanner

* bump pypi version

* name change
2019-03-04 22:57:45 -08:00
Diogo Gomes
efe4ce9a05 check we have a tb (#21670) 2019-03-05 06:18:25 +01:00
ktnrg45
efa5d5dfe3 Add support for multiple devices for PS4 component (#21302)
* Support multiple devices.

* Revert "Support multiple devices."

This reverts commit 3f5d4462a98da13ebb1ab1c07d341dbd7020e6cc.

* Support multiple devices

* Bump to 0.3.3

* bump 0.3.4

* Add tests for multiple devices.

* Update Requirements

* Update config_flow.py

* Update config_flow.py

* fixed typo

* Reordered functions

* Added multiple flow implementation test.

* fix

* typo

* fix tests

* bump 0.4.0

* Bump 0.4.0

* 0.4.0

* bump version

* bump version

* bump version

* Add keep alive feature with multiple devices

* bump version

* bump version

* bump version

* bump 0.4.7

* bump 0.4.7

* bump 0.4.7

* Edited tests.

* bump/pylint

* pylint

* bump/pylint

* bump/pylint

* Change to add additional entry

* Changed to multiple entries

* pylint

* Corrections to manage multiple devices.

* lint

* comments

* Removed redundant for loop

* Shorthand correction

* Remove reference to private object

* Test fix

* Revert changes. Test failure.

* Test fix

* test fix

* unindent assertions

* pylint
2019-03-05 01:48:25 +01:00
Paulus Schoutsen
3135257c0d Bumped version to 0.89.0b2 2019-03-04 16:02:05 -08:00
Paulus Schoutsen
b20b811cb9 Avoid recorder thread crashing (#21668) 2019-03-04 16:01:59 -08:00
Paulus Schoutsen
df25128923 Avoid recorder thread crashing (#21668) 2019-03-04 16:01:31 -08:00
Franck Nijhof
a778cd117f Upgrade toonapilib to 3.1.0 (#21661) 2019-03-04 16:01:15 -08:00
Franck Nijhof
31b88197eb 🚑 Fixes Toon doing I/O in coroutines (#21657) 2019-03-04 16:01:14 -08:00
Paulus Schoutsen
81c252f917 Rename Google Assistant evenets (#21655) 2019-03-04 16:01:13 -08:00
Franck Nijhof
f5a0b5ab98 👕 Corrects unit of measurement symbol for Watt (#21654) 2019-03-04 16:01:12 -08:00
Gijs Reichert
a382ba731d Cast displaytime to int for JSON RPC (#21649) 2019-03-04 15:59:19 -08:00
Paulus Schoutsen
cca8d4c951 Fix calc next (#21630) 2019-03-04 15:59:18 -08:00
Anders Melchiorsen
932080656d Upgrade pysonos to 0.0.8 (#21624) 2019-03-04 15:59:18 -08:00
Jason Hu
d5bdfdb0b3 Resolve race condition when HA auth provider is loading (#21619)
* Resolve race condition when HA auth provider is loading

* Fix

* Add more tests

* Lint
2019-03-04 15:59:17 -08:00
Andrew Sayre
d9806f759b Handle when installed app has already been removed (#21595) 2019-03-04 15:59:16 -08:00
Anders Melchiorsen
e6debe09e8 Word the tplink deprecation warning more strongly (#21586) 2019-03-04 15:59:16 -08:00
Jason Hu
c5dad82211 Log exception occurred in WS service call command (#21584) 2019-03-04 15:59:15 -08:00
Daniel Høyer Iversen
ec9ccf6402 Upgrade PyXiaomiGateway library (#21582) 2019-03-04 15:59:15 -08:00
Jason Hu
a268aab2ec Re-thrown exception occurred in the blocking service call (#21573)
* Rethrown exception occurred in the actual service call

* Fix lint and test
2019-03-04 15:59:14 -08:00
damarco
996e0a6389 Bump zigpy-deconz (#21566) 2019-03-04 15:59:14 -08:00
emontnemery
e877983533 Make time trigger data trigger.now local (#21544)
* Make time trigger data trigger.now local

* Make time pattern trigger data trigger.now local

* Lint

* Rework according to review comment

* Lint
2019-03-04 15:59:13 -08:00
Robbie Trencheny
73675d5a48 mobile_app component (#21475)
* Initial pass of a mobile_app component

* Fully support encryption, validation for the webhook payloads, and other general improvements

* Return same format as original API calls

* Minor encryption fixes, logging improvements

* Migrate Owntracks to use the superior PyNaCl instead of libnacl, mark it as a requirement in mobile_app

* Add mobile_app to .coveragerc

* Dont manually b64decode on OT

* Initial requested changes

* Round two of fixes

* Initial mobile_app tests

* Dont allow making registration requests for same/existing device

* Test formatting fixes

* Add mobile_app to default_config

* Add some more keys allowed in registration payloads

* Add support for getting a single device, updating a device, getting all devices. Also change from /api/mobile_app/register to /api/mobile_app/devices

* Change device_id to fingerprint

* Next round of changes

* Add keyword args and pass context on all relevant calls

* Remove SingleDeviceView in favor of webhook type to update registration

* Only allow some properties to be updated on registrations, rename integration_data to app_data

* Add call service test, ensure events actually fire, only run the encryption tests if sodium is installed

* pylint

* Fix OwnTracks test

* Fix iteration of devices and remove device_for_webhook_id
2019-03-04 15:59:12 -08:00
roblandry
e10e27d809 Add SmartThingsAccelCluster to ZHA binary_sensor (#21609)
* Add SmartThingsAccelCluster to binary_sensor

* Make corrections per discussion with @dmulcahey

* Add missing const to gateway.py

* Remove Acceleration from no sensor
2019-03-04 15:56:05 -08:00
Jason Hu
4a3b4cf346 Resolve race condition when HA auth provider is loading (#21619)
* Resolve race condition when HA auth provider is loading

* Fix

* Add more tests

* Lint
2019-03-04 15:55:26 -08:00
Daniel Høyer Iversen
7a7080055e Netatmo, handle missing thermostat devices (#21651) 2019-03-04 15:54:21 -08:00
Franck Nijhof
955b71c44b 👕 Corrects unit of measurement symbol for Watt (#21654) 2019-03-04 15:53:16 -08:00
Franck Nijhof
73b100d3af 🚑 Fixes Toon doing I/O in coroutines (#21657) 2019-03-04 15:52:00 -08:00
Franck Nijhof
641138a986 Upgrade toonapilib to 3.1.0 (#21661) 2019-03-04 15:51:15 -08:00
Paulus Schoutsen
27e8a6ee80 Rename Google Assistant evenets (#21655) 2019-03-04 13:18:16 -08:00
Alok Saboo
5375510535 Add camera name to logs (#21653) 2019-03-04 12:06:28 -08:00
Paulus Schoutsen
8213016eaf Allow targeting areas in service calls (#21472)
* Allow targeting areas in service calls

* Lint + Type

* Address comments
2019-03-04 09:51:12 -08:00
Nate Clark
f62eb22ef8 Add support for DHT and DS18B20 sensors via Konnected firmware (#21189)
* mvp basic temperature sensor support

* support for DHT temperature & humidity

* add support for ds18b20 sensors

* improve resolution of device settings

* update requirements_all.txt

* re-organize new file

* don't use filter(lambda: syntax

* set unique_id on entities to allow renaming in the UI

* leverage base Entity module to do C to F conversion

* add option for setting poll_interval

* use handler pattern to handle updates from Konnected device

* cleanups from code review
2019-03-04 16:56:41 +01:00
Gijs Reichert
158e25562b Cast displaytime to int for JSON RPC (#21649) 2019-03-04 15:25:28 +01:00
Pascal Vizeli
72b6e80d02 Better output of workflow 2019-03-04 14:06:05 +01:00
Fabian Affolter
eb1d7be67c Upgrade youtube_dl to 2019.03.01 (#21647) 2019-03-04 14:00:10 +01:00
Pascal Vizeli
c60627c699 GitHub Workflow (#21643)
* Fix tox.ini

* Update main.workflow

* Update tox.ini

* Update main.workflow

* Update main.workflow

* Update tox.ini

* Try only with one

* Update main.workflow

* Update main.workflow

* Update main.workflow

* Update main.workflow

* Update main.workflow
2019-03-04 12:36:50 +01:00
Pascal Vizeli
5c9f266672 Fix actions with tox (#21642)
* Fix actions with tox

* Update main.workflow
2019-03-04 10:23:19 +01:00
Pascal Vizeli
5efcbc5043 Replace travis (#21641) 2019-03-04 09:59:35 +01:00
Nick Whyte
10e334cbf0 Allow configuration of update interval for ness_alarm (#21415)
* ness_alarm: Allow configuration of update_interval

* requirements

* update_interval -> scan_interval

* Consistent config validation

* requirements

* don't touch dependency version
2019-03-04 00:05:44 -08:00
Paulus Schoutsen
43f85f7053 Updated frontend to 20190303.0 2019-03-03 22:41:38 -08:00
Paulus Schoutsen
c03116291e Updated frontend to 20190303.0 2019-03-03 21:55:51 -08:00
Paulus Schoutsen
de9e6e8d1a Update translations 2019-03-03 21:53:45 -08:00
David F. Mulcahey
fc07d3a159 Add storage helper to ZHA and use it for the device node descriptor (#21500)
* node descriptor implementation

add info to device info

disable pylint rule

check for success

* review comments

* send manufacturer code for get attr value for mfg clusters

* ST report configs

* do zdo task first

* add guard

* use faster reporting config

* disable false positive pylint
2019-03-03 21:22:42 -08:00
Jason Hu
ee6f09dd29 Log exception occurred in WS service call command (#21584) 2019-03-03 21:22:22 -08:00
Paulus Schoutsen
48a2e50f84 Fix calc next (#21630) 2019-03-03 21:36:13 -07:00
Paulus Schoutsen
f5ed6432eb Expose create/delete cloudhook (#21606)
* Expose create/delete cloudhook

* Make sure we dont publish cloudhooks when not connected
2019-03-03 19:03:49 -08:00
shanbs
c25cbccca9 Return Netatmo climate operation_mode instead of boiler status (#21633)
* Merge the devices into one list and add into entries at once; Return operation_mode instead of boiler status.

* Removing property operation_mode
2019-03-04 02:42:29 +01:00
Aaron Bach
31bcf6c35f Bump pyflunearyou to 1.0.3 (#21634) 2019-03-03 18:39:13 -07:00
Willem Burgers
2017e45d78 fix derived rate, fixes #20097 (#21620)
* fix derived rate, fixes #20097

* fix derived rate, fixes #20097

* Fix typo

thnx @amelchio

* Make the test more realistic

Took values from my own smart meter for the test

* Update test to ignore rounding issues
2019-03-03 23:42:52 +01:00
srirams
818776d2b4 Add optional sender name for SendGrid (#21610)
* Set "Home Assistant" as email sender name for SendGrid

* make sender name configurable

* sendgrid tweaks

* fix config
2019-03-03 13:44:40 -08:00
Jeff Irion
fa938f5628 Add 'app_name' property and 'apps' config entry to Fire TV (#21601)
* Add 'app_name' property and 'apps' config entry to Fire TV

* Define 'CONF_APPS', don't import it

* Address reviewer comments
2019-03-03 12:39:39 -08:00
Markus Jankowski
3032283b99 Add device HMIP-eTRV-C to HomematicIP (#21612)
* Update dependencies

* Add additional device HMIP-eTRV-C

add valveActualTemperature to HeatingThermostats (HMIP-eTRV-C, HMIP-eTRV, HMIP-eTRV-2)

* Removed HomematicipThermostatTemperatureSensor

already in climate
2019-03-03 12:33:48 -08:00
kennedyshead
1308ead8d6 Bumping aioasuswrt (#21627) 2019-03-03 12:31:09 -08:00
Andrew Sayre
3e0459cef9 SmartThings remove SmartApp/Automation on integration removal (#21594)
* Add clean-up logic upon entry removal

* Removed unecessary app removal from migration

* Change log level and clarified code
2019-03-03 13:47:25 -06:00
Anders Melchiorsen
0f6e0aa355 Upgrade pysonos to 0.0.8 (#21624) 2019-03-03 18:49:29 +01:00
Maikel Punie
b985223603 Add the velbus sync clock service (#21308)
* Add the velbus sync clock service

* Fixed houndci-bot commants

* Fix lint and pylint

* fixed all comments

* Hound bot comments

* Fix for flake8
2019-03-03 06:37:36 -06:00
Andrew Sayre
1e60993aa7 Handle when installed app has already been removed (#21595) 2019-03-02 21:57:57 -06:00
shanbs
18372ad81b Added support for multiple Netatmo thermostats/valves (#19407)
* climate/netatmo: Added support for muletiple thermostats/valves

* Adjusted the update interval throttle to 10 seconds

* Avoid returning 'homes' without 'therm_schedules'

* Requires home to have 'modules' as well as 'therm_schedules'; Using pyatmo 1.7

* Support multiple homes

* Fix nest level too deep issue

* Fix crashing bug when discovery is true

* Fix crashing bug when discovery is true

* Modifications according to review comments

* Resolve format issue

* Fix mode name issue

* Revisions according to review's suggestions

* Revisions according to review's comments

* Revisions according to review's comments
2019-03-02 19:51:42 -08:00
Matt White
18491c515f Further Yale ZWave lock device mapping cleanup (#21128)
* Update device mapping for workarounds from zwave device db

* Update comment on old Yale vendor ID
2019-03-02 20:29:16 -06:00
Rohan Kapoor
9af8c95e83 Upgrade motorparts to 1.1.0 (#21602) 2019-03-02 20:23:17 -06:00
ehendrix23
833f17de04 Add parameter hold_secs for Harmony remote send command (#19650)
* Update requirements

Updated requirements

* Small bump for aioharmony

Small version bump increase for aioharmony

* Add attributes

Add firmware and config version attributes

* Add hold for button press on send_command

* Fix requirements file

For some reason aioharmony ended up in there as a duplicate. Fixed it.

* Revert rebase changes

Revert some changes that should have been reverted back as part of rebase.

* Updated based on review

Removed HOLD_SECS from platform schema (configuration)
Updated getting kwargs in async_send_command
Updated debug log to include delay_secs
2019-03-03 00:54:03 +01:00
Daniel Perna
b8eebda541 Update pyhomematic (#21600) 2019-03-02 17:42:51 -05:00
David F. Mulcahey
45316f6ed6 ZHA fixes (#21592)
* do not report on 0x1000 LightLink cluster
* don't flood Zigbee network during configuration or initialization
* add lifeline of 60 minutes to lights
* use ootb polling
2019-03-02 14:09:01 -05:00
Anders Melchiorsen
5eab86986e Word the tplink deprecation warning more strongly (#21586) 2019-03-02 12:32:18 +01:00
Joe Trabulsy
61e4a6be18 Update for new pyvesyncv_v2 library and vesync switch support (#21449)
* Change dependency to pyvesync-v2 for vesync switch

* Update requirements_all.txt

* Update Version - Wall Switch Support

Update required version for vesync outlets and switches.  Eliminate API call for energy usage for wall switches that do not have that feature

* fix name convention
2019-03-02 11:57:10 +01:00
Diogo Gomes
1ad4779443 Add network throughput statistics to systemmonitor sensor (#21575)
* add network throughput

* lint
2019-03-02 16:08:15 +05:30
Diogo Gomes
ed2b9e5483 Centrally define Watt (#21570)
* centralize Watt definition

* lint
2019-03-02 11:29:59 +01:00
Kevin Fronczak
e55ce61100 Upgrade blinkpy==0.13.1 (Fixes #21559) (#21578)
* Upgrade blinkpy with new api endpoint

* Change wifi units to dBm
2019-03-02 11:28:44 +01:00
Daniel Høyer Iversen
8e75bfb11e Upgrade PyXiaomiGateway library (#21582) 2019-03-02 11:27:36 +01:00
Penny Wood
f61f650495 Get room hints from areas (#21519)
* Get google room hint from area.

* Test case for area code.

* Updates as per code review.
2019-03-01 23:31:57 -08:00
Anders Melchiorsen
0c8a31b8ec Memory optimization for logbook (#21549) 2019-03-01 23:23:45 -08:00
Jason Hu
f1b867dccb Re-thrown exception occurred in the blocking service call (#21573)
* Rethrown exception occurred in the actual service call

* Fix lint and test
2019-03-01 23:09:31 -08:00
emontnemery
cd89809be5 Make time trigger data trigger.now local (#21544)
* Make time trigger data trigger.now local

* Make time pattern trigger data trigger.now local

* Lint

* Rework according to review comment

* Lint
2019-03-01 23:09:12 -08:00
Robbie Trencheny
655ada1374 mobile_app component (#21475)
* Initial pass of a mobile_app component

* Fully support encryption, validation for the webhook payloads, and other general improvements

* Return same format as original API calls

* Minor encryption fixes, logging improvements

* Migrate Owntracks to use the superior PyNaCl instead of libnacl, mark it as a requirement in mobile_app

* Add mobile_app to .coveragerc

* Dont manually b64decode on OT

* Initial requested changes

* Round two of fixes

* Initial mobile_app tests

* Dont allow making registration requests for same/existing device

* Test formatting fixes

* Add mobile_app to default_config

* Add some more keys allowed in registration payloads

* Add support for getting a single device, updating a device, getting all devices. Also change from /api/mobile_app/register to /api/mobile_app/devices

* Change device_id to fingerprint

* Next round of changes

* Add keyword args and pass context on all relevant calls

* Remove SingleDeviceView in favor of webhook type to update registration

* Only allow some properties to be updated on registrations, rename integration_data to app_data

* Add call service test, ensure events actually fire, only run the encryption tests if sodium is installed

* pylint

* Fix OwnTracks test

* Fix iteration of devices and remove device_for_webhook_id
2019-03-01 23:08:20 -08:00
Andrew Sayre
0903bd92f0 Add config entry remove callback (#21576) 2019-03-01 21:13:55 -08:00
David F. Mulcahey
cd6c923123 fix exception (#21571) 2019-03-01 20:15:36 -05:00
Wagner Sartori Junior
b8ec74cc15 bump pyxeoma to 1.4.1 to fix and close #19306 (#21568) 2019-03-01 20:11:32 -05:00
damarco
1a9dcaefd2 Bump zigpy-deconz (#21566) 2019-03-01 13:47:20 -05:00
Paulus Schoutsen
52f337ef00 Allow chaining contexts (#21028)
* Allow chaining contexts

* Add stubbed out migration
2019-03-01 10:08:38 -08:00
David F. Mulcahey
b39846fb6b add friendly name to devices in the device registry (#21499)
switch to name_by_user

review comments

add device reg info to zha device api
2019-03-01 08:11:24 -05:00
msvinth
0aba49adce Add separate on/off ids on manual configured IHC lights (#20253)
* Add support for separate on/off ids on manual configured IHC lights.
This makes it easier to support IHC code units thats relies on being
turned on and off through specific inputs.
Also adds a pulse service (ihc.pulse) that supports sending a short on/off pulse to an IHC input.

* Fix

* Lint fix

* Add on/off id support in switch

* Make pulse async

* Code review fixes
2019-03-01 08:17:59 +01:00
Paulus Schoutsen
ee4be13bda Allow config entry reloading (#21502)
* Allow config entry reloading

* Fix duplicate test name

* Add comment

* fix typing
2019-02-28 22:27:20 -06:00
David F. Mulcahey
aa30ac52ea prevent duplicate event channel registration (#21534) 2019-02-28 22:53:59 -05:00
Aaron Bach
326513af90 Add pause/unpause services to RainMachine (#21548)
* Add pause/unpause services to RainMachine

* Update requirements
2019-02-28 17:58:39 -08:00
Paulus Schoutsen
5e67054ee1 Updated frontend to 20190228.0 2019-02-28 17:43:36 -08:00
David F. Mulcahey
901b2b4ba3 new websocket api way (#21533) 2019-02-28 19:32:41 -05:00
Aaron Bach
b8a94c30e8 Add watchdog to Ambient PWS (#21507)
* Add watchdog to Ambient PWS

* Better labeling

* Owner comments
2019-02-28 14:28:20 -08:00
Martin Hjelmare
40d7fbcda4 Clean up gpslogger tests (#21543) 2019-02-28 14:17:53 -08:00
Aaron Bach
8ebe5c61e8 Fix incorrect pyairvisual call (#21542) 2019-02-28 14:17:10 -08:00
Diogo Gomes
81dd2acf3b Update CODEOWNERS (#21545)
add myself to camera.push
2019-02-28 14:16:51 -08:00
Anders Melchiorsen
193cab4f62 Improve new Sonos snapshot/restore (#21509)
* Fine-tune new Sonos snapshot/restore

* Move into class
2019-02-28 10:25:31 -08:00
Jason Hu
b18b1cffff Fix warning (#21538) 2019-02-28 10:10:21 -08:00
cpopp
84b84559a4 Add support for homekit controller sensors (#21535)
Adds support for homekit devices with temperature, humidity, and
light level characteristics (such as the iHome iSS50)
2019-02-28 10:09:04 -08:00
David F. Mulcahey
82bdd9568d Add direct binding for remotes and lights for ZHA (#21498)
* cluster matching and binding apis

implement binding

callback

fix loop

fix loops

* review comments

* use any because it is clearer
2019-02-28 10:04:35 -08:00
Jason Hu
5ce4fe65b2 Allow skip-pip applied to HA core (#21527) 2019-02-28 10:01:10 -08:00
Paulus Schoutsen
4f4a8a61d2 Only use a single store instance (#21521) 2019-02-28 10:00:17 -08:00
Ben Randall
e14c8c788e Add PLATFORM_SCHEMA_BASE to telegram_bot component (#21155) 2019-02-28 18:27:40 +01:00
emontnemery
c340083ba5 Add missing retain option to mqtt.climate configuration schema (#21536) 2019-02-28 18:26:54 +01:00
Marco M
c3d4738649 Mqtt alarm added value_template and code_arm_required (#19558)
* Added value_template config for parsing json value from state topic

Added arm_code_required to avoid code enter when arming

* Renamed config parameter to code_arm_required

* Fix for discovery update compatibility

* Fixed lint error

* Added test
2019-02-28 17:44:23 +01:00
Fabian Affolter
b0dd6e4093 Upgrade python-mystrom to 0.5.0 (#21523) 2019-02-28 17:46:38 +05:30
Fabian Affolter
3e8e998078 Upgrade numpy to 1.16.2 (#21525) 2019-02-28 17:46:21 +05:30
Victor Vostrikov
342ddbfe8c Updated variable name for readability (#21528) 2019-02-28 17:35:39 +05:30
koolsb
bfc6f51b25 Add arm night for alarm decoder (#21488) 2019-02-28 17:15:17 +05:30
Jeff Irion
27a780dcc9 Register 'firetv.adb_command' service (#21419)
* Register 'media_player.firetv_adb_cmd' service

* Wrap the 'firetv_adb_cmd' service with 'adb_decorator'

* Address reviewer comments

* Move firetv to its own platform

* Move 'adb_command' service description

* Rename DOMAIN to FIRETV_DOMAIN

* Import KEYS in __init__ method

* Change 'self.KEYS' to 'self.keys'

* Update firetv in .coveragerc

* 'homeassistant.components.media_player.firetv' -> 'homeassistant.components.firetv'

* 'homeassistant.components.firetv' -> 'homeassistant.components.firetv.media_player'
2019-02-28 12:29:56 +01:00
Fabian Affolter
6f2def06be Upgrade opensensemap-api to 0.1.5 (#21524) 2019-02-28 11:59:14 +01:00
Paulus Schoutsen
229d19bb20 Fix lint (#21520) 2019-02-27 21:35:14 -08:00
Adam Dullage
548d7bbeda Bump starlingbank version to 3.1 (#21501)
* Bump starlingbank version to 3.1

Resolves Python 3.5 compatibility issue.

* Remove syntax error.
2019-02-28 09:53:21 +05:30
Aaron Bach
aad15776c0 Upgrade pyflunearyou to 1.0.2 (#21514) 2019-02-27 17:52:50 -07:00
Aaron Bach
c1365de861 Upgraded py17track to 2.2.2 (#21515) 2019-02-27 17:52:31 -07:00
Aaron Bach
fd32910185 Upgrade pytile to 2.0.6 (#21516) 2019-02-27 17:52:05 -07:00
Aaron Bach
1369b0b583 Upgrade pypollencom to 2.2.3 (#21517) 2019-02-27 17:51:36 -07:00
Aaron Bach
4ca7273c58 Upgrade pyopenuv to 1.0.9 (#21513) 2019-02-27 17:51:09 -07:00
539 changed files with 15742 additions and 5854 deletions

View File

@@ -27,6 +27,7 @@ omit =
homeassistant/components/ambient_station/*
homeassistant/components/amcrest/*
homeassistant/components/android_ip_webcam/*
homeassistant/components/androidtv/*
homeassistant/components/apcupsd/*
homeassistant/components/apiai/*
homeassistant/components/apple_tv/*
@@ -68,6 +69,7 @@ omit =
homeassistant/components/camera/xiaomi.py
homeassistant/components/camera/yi.py
homeassistant/components/cast/*
homeassistant/components/cisco_mobility_express/device_tracker.py
homeassistant/components/climate/coolmaster.py
homeassistant/components/climate/ephember.py
homeassistant/components/climate/eq3btsmart.py
@@ -109,6 +111,7 @@ omit =
homeassistant/components/device_tracker/bt_home_hub_5.py
homeassistant/components/device_tracker/bt_smarthub.py
homeassistant/components/device_tracker/cisco_ios.py
homeassistant/components/cppm_tracker/device_tracker.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/google_maps.py
@@ -138,6 +141,7 @@ omit =
homeassistant/components/device_tracker/trackr.py
homeassistant/components/device_tracker/ubee.py
homeassistant/components/device_tracker/ubus.py
homeassistant/components/device_tracker/xfinity.py
homeassistant/components/digital_ocean/*
homeassistant/components/dominos/*
homeassistant/components/doorbird/*
@@ -154,6 +158,7 @@ omit =
homeassistant/components/elkm1/*
homeassistant/components/emoncms_history/*
homeassistant/components/emulated_hue/upnp.py
homeassistant/components/enigma2/media_player.py
homeassistant/components/enocean/*
homeassistant/components/envisalink/*
homeassistant/components/esphome/__init__.py
@@ -233,7 +238,7 @@ omit =
homeassistant/components/light/limitlessled.py
homeassistant/components/light/lw12wifi.py
homeassistant/components/light/mystrom.py
homeassistant/components/light/nanoleaf_aurora.py
homeassistant/components/light/nanoleaf.py
homeassistant/components/light/niko_home_control.py
homeassistant/components/light/opple.py
homeassistant/components/light/osramlightify.py
@@ -280,7 +285,6 @@ omit =
homeassistant/components/media_player/dunehd.py
homeassistant/components/media_player/emby.py
homeassistant/components/media_player/epson.py
homeassistant/components/media_player/firetv.py
homeassistant/components/media_player/frontier_silicon.py
homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/gstreamer.py
@@ -320,6 +324,7 @@ omit =
homeassistant/components/media_player/yamaha.py
homeassistant/components/media_player/ziggo_mediabox_xl.py
homeassistant/components/meteo_france/*
homeassistant/components/mobile_app/*
homeassistant/components/mochad/*
homeassistant/components/modbus/*
homeassistant/components/mychevy/*
@@ -384,7 +389,7 @@ omit =
homeassistant/components/point/*
homeassistant/components/prometheus/*
homeassistant/components/ps4/__init__.py
homeassistant/components/ps4/media_player.py
homeassistant/components/ps4/media_player.py
homeassistant/components/qwikswitch/*
homeassistant/components/rachio/*
homeassistant/components/rainbird/*
@@ -631,6 +636,7 @@ omit =
homeassistant/components/thingspeak/*
homeassistant/components/thinkingcleaner/*
homeassistant/components/tibber/*
homeassistant/components/tof/sensor.py
homeassistant/components/toon/*
homeassistant/components/tplink_lte/*
homeassistant/components/tradfri/*
@@ -696,4 +702,4 @@ exclude_lines =
# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError
raise NotImplementedError

View File

@@ -1,3 +1,7 @@
## Breaking Change:
<!-- What is breaking and why we have to break it. Remove this section only if it was NOT a breaking change. -->
## Description:

41
.github/main.workflow vendored Normal file
View File

@@ -0,0 +1,41 @@
workflow "Python 3.7 - tox" {
resolves = ["Python 3.7 - tests"]
on = "push"
}
action "Python 3.7 - tests" {
uses = "home-assistant/actions/py37-tox@master"
args = "-e py37"
}
workflow "Python 3.6 - tox" {
resolves = ["Python 3.6 - tests"]
on = "push"
}
action "Python 3.6 - tests" {
uses = "home-assistant/actions/py36-tox@master"
args = "-e py36"
}
workflow "Python 3.5 - tox" {
resolves = ["Pyton 3.5 - typing"]
on = "push"
}
action "Python 3.5 - tests" {
uses = "home-assistant/actions/py35-tox@master"
args = "-e py35"
}
action "Python 3.5 - lints" {
uses = "home-assistant/actions/py35-tox@master"
needs = ["Python 3.5 - tests"]
args = "-e lint"
}
action "Pyton 3.5 - typing" {
uses = "home-assistant/actions/py35-tox@master"
args = "-e typing"
needs = ["Python 3.5 - lints"]
}

View File

@@ -1,8 +1,18 @@
sudo: false
dist: xenial
addons:
apt:
sources:
- sourceline: "ppa:jonathonf/ffmpeg-4"
packages:
- libudev-dev
- libavformat-dev
- libavcodec-dev
- libavdevice-dev
- libavutil-dev
- libswscale-dev
- libswresample-dev
- libavfilter-dev
matrix:
fast_finish: true
include:
@@ -19,15 +29,12 @@ matrix:
env: TOXENV=py36
- 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

@@ -51,6 +51,7 @@ homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
homeassistant/components/binary_sensor/hikvision.py @mezz64
homeassistant/components/binary_sensor/threshold.py @fabaff
homeassistant/components/binary_sensor/uptimerobot.py @ludeeus
homeassistant/components/camera/push.py @dgomes
homeassistant/components/camera/yi.py @bachya
homeassistant/components/climate/coolmaster.py @OnFreund
homeassistant/components/climate/ephember.py @ttroy50
@@ -62,12 +63,13 @@ homeassistant/components/cover/group.py @cdce8p
homeassistant/components/cover/template.py @PhracturedBlue
homeassistant/components/device_tracker/asuswrt.py @kennedyshead
homeassistant/components/device_tracker/automatic.py @armills
homeassistant/components/device_tracker/bt_smarthub.py @jxwolstenholme
homeassistant/components/device_tracker/huawei_router.py @abmantis
homeassistant/components/device_tracker/quantum_gateway.py @cisasteelersfan
homeassistant/components/device_tracker/tile.py @bachya
homeassistant/components/device_tracker/traccar.py @ludeeus
homeassistant/components/device_tracker/bt_smarthub.py @jxwolstenholme
homeassistant/components/device_tracker/synology_srm.py @aerialls
homeassistant/components/device_tracker/xfinity.py @cisasteelersfan
homeassistant/components/light/lifx_legacy.py @amelchio
homeassistant/components/light/yeelight.py @rytilahti
homeassistant/components/light/yeelightsunflower.py @lindsaymarkward
@@ -95,6 +97,7 @@ homeassistant/components/sensor/bitcoin.py @fabaff
homeassistant/components/sensor/cpuspeed.py @fabaff
homeassistant/components/sensor/cups.py @fabaff
homeassistant/components/sensor/darksky.py @fabaff
homeassistant/components/sensor/discogs.py @thibmaek
homeassistant/components/sensor/file.py @fabaff
homeassistant/components/sensor/filter.py @dgomes
homeassistant/components/sensor/fixer.py @fabaff
@@ -112,6 +115,7 @@ homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/min_max.py @fabaff
homeassistant/components/sensor/moon.py @fabaff
homeassistant/components/sensor/netdata.py @fabaff
homeassistant/components/sensor/nmbs.py @thibmaek
homeassistant/components/sensor/nsw_fuel_station.py @nickw444
homeassistant/components/sensor/pi_hole.py @fabaff
homeassistant/components/sensor/pollen.py @bachya

View File

@@ -100,9 +100,21 @@ class AuthManager:
"""Return a list of available auth modules."""
return list(self._mfa_modules.values())
def get_auth_provider(self, provider_type: str, provider_id: str) \
-> Optional[AuthProvider]:
"""Return an auth provider, None if not found."""
return self._providers.get((provider_type, provider_id))
def get_auth_providers(self, provider_type: str) \
-> List[AuthProvider]:
"""Return a List of auth provider of one type, Empty if not found."""
return [provider
for (p_type, _), provider in self._providers.items()
if p_type == provider_type]
def get_auth_mfa_module(self, module_id: str) \
-> Optional[MultiFactorAuthModule]:
"""Return an multi-factor auth module, None if not found."""
"""Return a multi-factor auth module, None if not found."""
return self._mfa_modules.get(module_id)
async def async_get_users(self) -> List[models.User]:
@@ -113,6 +125,11 @@ class AuthManager:
"""Retrieve a user."""
return await self._store.async_get_user(user_id)
async def async_get_owner(self) -> Optional[models.User]:
"""Retrieve the owner."""
users = await self.async_get_users()
return next((user for user in users if user.is_owner), None)
async def async_get_group(self, group_id: str) -> Optional[models.Group]:
"""Retrieve all groups."""
return await self._store.async_get_group(group_id)

View File

@@ -11,13 +11,14 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.util import dt as dt_util
from . import models
from .const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY
from .const import GROUP_ID_ADMIN, GROUP_ID_USER, GROUP_ID_READ_ONLY
from .permissions import PermissionLookup, system_policies
from .permissions.types import PolicyType # noqa: F401
STORAGE_VERSION = 1
STORAGE_KEY = 'auth'
GROUP_NAME_ADMIN = 'Administrators'
GROUP_NAME_USER = "Users"
GROUP_NAME_READ_ONLY = 'Read Only'
@@ -38,6 +39,7 @@ class AuthStore:
self._perm_lookup = None # type: Optional[PermissionLookup]
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
private=True)
self._lock = asyncio.Lock()
async def async_get_groups(self) -> List[models.Group]:
"""Retrieve all users."""
@@ -272,8 +274,16 @@ class AuthStore:
async def _async_load(self) -> None:
"""Load the users."""
[ent_reg, data] = await asyncio.gather(
async with self._lock:
if self._users is not None:
return
await self._async_load_task()
async def _async_load_task(self) -> None:
"""Load the users."""
[ent_reg, dev_reg, data] = await asyncio.gather(
self.hass.helpers.entity_registry.async_get_registry(),
self.hass.helpers.device_registry.async_get_registry(),
self._store.async_load(),
)
@@ -282,7 +292,9 @@ class AuthStore:
if self._users is not None:
return
self._perm_lookup = perm_lookup = PermissionLookup(ent_reg)
self._perm_lookup = perm_lookup = PermissionLookup(
ent_reg, dev_reg
)
if data is None:
self._set_defaults()
@@ -297,6 +309,7 @@ class AuthStore:
# 1. Data from a recent version which has a single group without policy
# 2. Data from old version which has no groups
has_admin_group = False
has_user_group = False
has_read_only_group = False
group_without_policy = None
@@ -314,6 +327,13 @@ class AuthStore:
policy = system_policies.ADMIN_POLICY
system_generated = True
elif group_dict['id'] == GROUP_ID_USER:
has_user_group = True
name = GROUP_NAME_USER
policy = system_policies.USER_POLICY
system_generated = True
elif group_dict['id'] == GROUP_ID_READ_ONLY:
has_read_only_group = True
@@ -361,6 +381,10 @@ class AuthStore:
read_only_group = _system_read_only_group()
groups[read_only_group.id] = read_only_group
if not has_user_group:
user_group = _system_user_group()
groups[user_group.id] = user_group
for user_dict in data['users']:
# Collect the users group.
user_groups = []
@@ -475,7 +499,7 @@ class AuthStore:
'name': group.name
} # type: Dict[str, Any]
if group.id not in (GROUP_ID_READ_ONLY, GROUP_ID_ADMIN):
if not group.system_generated:
g_dict['policy'] = group.policy
groups.append(g_dict)
@@ -528,6 +552,8 @@ class AuthStore:
groups = OrderedDict() # type: Dict[str, models.Group]
admin_group = _system_admin_group()
groups[admin_group.id] = admin_group
user_group = _system_user_group()
groups[user_group.id] = user_group
read_only_group = _system_read_only_group()
groups[read_only_group.id] = read_only_group
self._groups = groups
@@ -543,6 +569,16 @@ def _system_admin_group() -> models.Group:
)
def _system_user_group() -> models.Group:
"""Create system user group."""
return models.Group(
name=GROUP_NAME_USER,
id=GROUP_ID_USER,
policy=system_policies.USER_POLICY,
system_generated=True,
)
def _system_read_only_group() -> models.Group:
"""Create read only group."""
return models.Group(

View File

@@ -5,4 +5,5 @@ ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
MFA_SESSION_EXPIRATION = timedelta(minutes=5)
GROUP_ID_ADMIN = 'system-admin'
GROUP_ID_USER = 'system-users'
GROUP_ID_READ_ONLY = 'system-read-only'

View File

@@ -2,6 +2,7 @@
Sending HOTP through notify service
"""
import asyncio
import logging
from collections import OrderedDict
from typing import Any, Dict, Optional, List
@@ -90,6 +91,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
self._include = config.get(CONF_INCLUDE, [])
self._exclude = config.get(CONF_EXCLUDE, [])
self._message_template = config[CONF_MESSAGE]
self._init_lock = asyncio.Lock()
@property
def input_schema(self) -> vol.Schema:
@@ -98,15 +100,19 @@ class NotifyAuthModule(MultiFactorAuthModule):
async def _async_load(self) -> None:
"""Load stored data."""
data = await self._user_store.async_load()
async with self._init_lock:
if self._user_settings is not None:
return
if data is None:
data = {STORAGE_USERS: {}}
data = await self._user_store.async_load()
self._user_settings = {
user_id: NotifySetting(**setting)
for user_id, setting in data.get(STORAGE_USERS, {}).items()
}
if data is None:
data = {STORAGE_USERS: {}}
self._user_settings = {
user_id: NotifySetting(**setting)
for user_id, setting in data.get(STORAGE_USERS, {}).items()
}
async def _async_save(self) -> None:
"""Save data."""

View File

@@ -1,4 +1,5 @@
"""Time-based One Time Password auth module."""
import asyncio
import logging
from io import BytesIO
from typing import Any, Dict, Optional, Tuple # noqa: F401
@@ -68,6 +69,7 @@ class TotpAuthModule(MultiFactorAuthModule):
self._users = None # type: Optional[Dict[str, str]]
self._user_store = hass.helpers.storage.Store(
STORAGE_VERSION, STORAGE_KEY, private=True)
self._init_lock = asyncio.Lock()
@property
def input_schema(self) -> vol.Schema:
@@ -76,12 +78,16 @@ class TotpAuthModule(MultiFactorAuthModule):
async def _async_load(self) -> None:
"""Load stored data."""
data = await self._user_store.async_load()
async with self._init_lock:
if self._users is not None:
return
if data is None:
data = {STORAGE_USERS: {}}
data = await self._user_store.async_load()
self._users = data.get(STORAGE_USERS, {})
if data is None:
data = {STORAGE_USERS: {}}
self._users = data.get(STORAGE_USERS, {})
async def _async_save(self) -> None:
"""Save data."""

View File

@@ -1,12 +1,14 @@
"""Entity permissions."""
from functools import wraps
from typing import Callable, List, Union # noqa: F401
from collections import OrderedDict
from typing import Callable, Optional # noqa: F401
import voluptuous as vol
from .const import SUBCAT_ALL, POLICY_READ, POLICY_CONTROL, POLICY_EDIT
from .models import PermissionLookup
from .types import CategoryType, ValueType
from .types import CategoryType, SubCategoryDict, ValueType
# pylint: disable=unused-import
from .util import SubCatLookupType, lookup_all, compile_policy # noqa
SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({
vol.Optional(POLICY_READ): True,
@@ -15,6 +17,7 @@ SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({
}))
ENTITY_DOMAINS = 'domains'
ENTITY_AREAS = 'area_ids'
ENTITY_DEVICE_IDS = 'device_ids'
ENTITY_ENTITY_IDS = 'entity_ids'
@@ -24,148 +27,65 @@ ENTITY_VALUES_SCHEMA = vol.Any(True, vol.Schema({
ENTITY_POLICY_SCHEMA = vol.Any(True, vol.Schema({
vol.Optional(SUBCAT_ALL): SINGLE_ENTITY_SCHEMA,
vol.Optional(ENTITY_AREAS): ENTITY_VALUES_SCHEMA,
vol.Optional(ENTITY_DEVICE_IDS): ENTITY_VALUES_SCHEMA,
vol.Optional(ENTITY_DOMAINS): ENTITY_VALUES_SCHEMA,
vol.Optional(ENTITY_ENTITY_IDS): ENTITY_VALUES_SCHEMA,
}))
def _entity_allowed(schema: ValueType, key: str) \
-> Union[bool, None]:
"""Test if an entity is allowed based on the keys."""
if schema is None or isinstance(schema, bool):
return schema
assert isinstance(schema, dict)
return schema.get(key)
def _lookup_domain(perm_lookup: PermissionLookup,
domains_dict: SubCategoryDict,
entity_id: str) -> Optional[ValueType]:
"""Look up entity permissions by domain."""
return domains_dict.get(entity_id.split(".", 1)[0])
def _lookup_area(perm_lookup: PermissionLookup, area_dict: SubCategoryDict,
entity_id: str) -> Optional[ValueType]:
"""Look up entity permissions by area."""
entity_entry = perm_lookup.entity_registry.async_get(entity_id)
if entity_entry is None or entity_entry.device_id is None:
return None
device_entry = perm_lookup.device_registry.async_get(
entity_entry.device_id
)
if device_entry is None or device_entry.area_id is None:
return None
return area_dict.get(device_entry.area_id)
def _lookup_device(perm_lookup: PermissionLookup,
devices_dict: SubCategoryDict,
entity_id: str) -> Optional[ValueType]:
"""Look up entity permissions by device."""
entity_entry = perm_lookup.entity_registry.async_get(entity_id)
if entity_entry is None or entity_entry.device_id is None:
return None
return devices_dict.get(entity_entry.device_id)
def _lookup_entity_id(perm_lookup: PermissionLookup,
entities_dict: SubCategoryDict,
entity_id: str) -> Optional[ValueType]:
"""Look up entity permission by entity id."""
return entities_dict.get(entity_id)
def compile_entities(policy: CategoryType, perm_lookup: PermissionLookup) \
-> Callable[[str, str], bool]:
"""Compile policy into a function that tests policy."""
# None, Empty Dict, False
if not policy:
def apply_policy_deny_all(entity_id: str, key: str) -> bool:
"""Decline all."""
return False
subcategories = OrderedDict() # type: SubCatLookupType
subcategories[ENTITY_ENTITY_IDS] = _lookup_entity_id
subcategories[ENTITY_DEVICE_IDS] = _lookup_device
subcategories[ENTITY_AREAS] = _lookup_area
subcategories[ENTITY_DOMAINS] = _lookup_domain
subcategories[SUBCAT_ALL] = lookup_all
return apply_policy_deny_all
if policy is True:
def apply_policy_allow_all(entity_id: str, key: str) -> bool:
"""Approve all."""
return True
return apply_policy_allow_all
assert isinstance(policy, dict)
domains = policy.get(ENTITY_DOMAINS)
device_ids = policy.get(ENTITY_DEVICE_IDS)
entity_ids = policy.get(ENTITY_ENTITY_IDS)
all_entities = policy.get(SUBCAT_ALL)
funcs = [] # type: List[Callable[[str, str], Union[None, bool]]]
# The order of these functions matter. The more precise are at the top.
# If a function returns None, they cannot handle it.
# If a function returns a boolean, that's the result to return.
# Setting entity_ids to a boolean is final decision for permissions
# So return right away.
if isinstance(entity_ids, bool):
def allowed_entity_id_bool(entity_id: str, key: str) -> bool:
"""Test if allowed entity_id."""
return entity_ids # type: ignore
return allowed_entity_id_bool
if entity_ids is not None:
def allowed_entity_id_dict(entity_id: str, key: str) \
-> Union[None, bool]:
"""Test if allowed entity_id."""
return _entity_allowed(
entity_ids.get(entity_id), key) # type: ignore
funcs.append(allowed_entity_id_dict)
if isinstance(device_ids, bool):
def allowed_device_id_bool(entity_id: str, key: str) \
-> Union[None, bool]:
"""Test if allowed device_id."""
return device_ids
funcs.append(allowed_device_id_bool)
elif device_ids is not None:
def allowed_device_id_dict(entity_id: str, key: str) \
-> Union[None, bool]:
"""Test if allowed device_id."""
entity_entry = perm_lookup.entity_registry.async_get(entity_id)
if entity_entry is None or entity_entry.device_id is None:
return None
return _entity_allowed(
device_ids.get(entity_entry.device_id), key # type: ignore
)
funcs.append(allowed_device_id_dict)
if isinstance(domains, bool):
def allowed_domain_bool(entity_id: str, key: str) \
-> Union[None, bool]:
"""Test if allowed domain."""
return domains
funcs.append(allowed_domain_bool)
elif domains is not None:
def allowed_domain_dict(entity_id: str, key: str) \
-> Union[None, bool]:
"""Test if allowed domain."""
domain = entity_id.split(".", 1)[0]
return _entity_allowed(domains.get(domain), key) # type: ignore
funcs.append(allowed_domain_dict)
if isinstance(all_entities, bool):
def allowed_all_entities_bool(entity_id: str, key: str) \
-> Union[None, bool]:
"""Test if allowed domain."""
return all_entities
funcs.append(allowed_all_entities_bool)
elif all_entities is not None:
def allowed_all_entities_dict(entity_id: str, key: str) \
-> Union[None, bool]:
"""Test if allowed domain."""
return _entity_allowed(all_entities, key)
funcs.append(allowed_all_entities_dict)
# Can happen if no valid subcategories specified
if not funcs:
def apply_policy_deny_all_2(entity_id: str, key: str) -> bool:
"""Decline all."""
return False
return apply_policy_deny_all_2
if len(funcs) == 1:
func = funcs[0]
@wraps(func)
def apply_policy_func(entity_id: str, key: str) -> bool:
"""Apply a single policy function."""
return func(entity_id, key) is True
return apply_policy_func
def apply_policy_funcs(entity_id: str, key: str) -> bool:
"""Apply several policy functions."""
for func in funcs:
result = func(entity_id, key)
if result is not None:
return result
return False
return apply_policy_funcs
return compile_policy(policy, subcategories, perm_lookup)

View File

@@ -8,6 +8,9 @@ if TYPE_CHECKING:
from homeassistant.helpers import ( # noqa
entity_registry as ent_reg,
)
from homeassistant.helpers import ( # noqa
device_registry as dev_reg,
)
@attr.s(slots=True)
@@ -15,3 +18,4 @@ class PermissionLookup:
"""Class to hold data for permission lookups."""
entity_registry = attr.ib(type='ent_reg.EntityRegistry')
device_registry = attr.ib(type='dev_reg.DeviceRegistry')

View File

@@ -5,6 +5,10 @@ ADMIN_POLICY = {
CAT_ENTITIES: True,
}
USER_POLICY = {
CAT_ENTITIES: True,
}
READ_ONLY_POLICY = {
CAT_ENTITIES: {
SUBCAT_ALL: {

View File

@@ -10,9 +10,11 @@ ValueType = Union[
None
]
# Example: entities.domains = { light: … }
SubCategoryDict = Mapping[str, ValueType]
SubCategoryType = Union[
# Example: entities.domains = { light: … }
Mapping[str, ValueType],
SubCategoryDict,
bool,
None
]

View File

@@ -0,0 +1,98 @@
"""Helpers to deal with permissions."""
from functools import wraps
from typing import Callable, Dict, List, Optional, Union, cast # noqa: F401
from .models import PermissionLookup
from .types import CategoryType, SubCategoryDict, ValueType
LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str],
Optional[ValueType]]
SubCatLookupType = Dict[str, LookupFunc]
def lookup_all(perm_lookup: PermissionLookup, lookup_dict: SubCategoryDict,
object_id: str) -> ValueType:
"""Look up permission for all."""
# In case of ALL category, lookup_dict IS the schema.
return cast(ValueType, lookup_dict)
def compile_policy(
policy: CategoryType, subcategories: SubCatLookupType,
perm_lookup: PermissionLookup
) -> Callable[[str, str], bool]: # noqa
"""Compile policy into a function that tests policy.
Subcategories are mapping key -> lookup function, ordered by highest
priority first.
"""
# None, False, empty dict
if not policy:
def apply_policy_deny_all(entity_id: str, key: str) -> bool:
"""Decline all."""
return False
return apply_policy_deny_all
if policy is True:
def apply_policy_allow_all(entity_id: str, key: str) -> bool:
"""Approve all."""
return True
return apply_policy_allow_all
assert isinstance(policy, dict)
funcs = [] # type: List[Callable[[str, str], Union[None, bool]]]
for key, lookup_func in subcategories.items():
lookup_value = policy.get(key)
# If any lookup value is `True`, it will always be positive
if isinstance(lookup_value, bool):
return lambda object_id, key: True
if lookup_value is not None:
funcs.append(_gen_dict_test_func(
perm_lookup, lookup_func, lookup_value))
if len(funcs) == 1:
func = funcs[0]
@wraps(func)
def apply_policy_func(object_id: str, key: str) -> bool:
"""Apply a single policy function."""
return func(object_id, key) is True
return apply_policy_func
def apply_policy_funcs(object_id: str, key: str) -> bool:
"""Apply several policy functions."""
for func in funcs:
result = func(object_id, key)
if result is not None:
return result
return False
return apply_policy_funcs
def _gen_dict_test_func(
perm_lookup: PermissionLookup,
lookup_func: LookupFunc,
lookup_dict: SubCategoryDict
) -> Callable[[str, str], Optional[bool]]: # noqa
"""Generate a lookup function."""
def test_value(object_id: str, key: str) -> Optional[bool]:
"""Test if permission is allowed based on the keys."""
schema = lookup_func(
perm_lookup, lookup_dict, object_id) # type: ValueType
if schema is None or isinstance(schema, bool):
return schema
assert isinstance(schema, dict)
return schema.get(key)
return test_value

View File

@@ -1,4 +1,5 @@
"""Home Assistant auth provider."""
import asyncio
import base64
from collections import OrderedDict
import logging
@@ -204,15 +205,21 @@ class HassAuthProvider(AuthProvider):
DEFAULT_TITLE = 'Home Assistant Local'
data = None
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize an Home Assistant auth provider."""
super().__init__(*args, **kwargs)
self.data = None # type: Optional[Data]
self._init_lock = asyncio.Lock()
async def async_initialize(self) -> None:
"""Initialize the auth provider."""
if self.data is not None:
return
async with self._init_lock:
if self.data is not None:
return
self.data = Data(self.hass)
await self.data.async_load()
data = Data(self.hass)
await data.async_load()
self.data = data
async def async_login_flow(
self, context: Optional[Dict]) -> LoginFlow:

View File

@@ -4,27 +4,23 @@ Support Legacy API password auth provider.
It will be removed when auth system production ready
"""
import hmac
from typing import Any, Dict, Optional, cast, TYPE_CHECKING
from typing import Any, Dict, Optional, cast
import voluptuous as vol
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from .. import AuthManager
from ..models import Credentials, UserMeta, User
if TYPE_CHECKING:
from homeassistant.components.http import HomeAssistantHTTP # noqa: F401
USER_SCHEMA = vol.Schema({
vol.Required('username'): str,
})
AUTH_PROVIDER_TYPE = 'legacy_api_password'
CONF_API_PASSWORD = 'api_password'
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
vol.Required(CONF_API_PASSWORD): cv.string,
}, extra=vol.PREVENT_EXTRA)
LEGACY_USER_NAME = 'Legacy API password user'
@@ -34,40 +30,45 @@ class InvalidAuthError(HomeAssistantError):
"""Raised when submitting invalid authentication."""
async def async_get_user(hass: HomeAssistant) -> User:
"""Return the legacy API password user."""
async def async_validate_password(hass: HomeAssistant, password: str)\
-> Optional[User]:
"""Return a user if password is valid. None if not."""
auth = cast(AuthManager, hass.auth) # type: ignore
found = None
for prv in auth.auth_providers:
if prv.type == 'legacy_api_password':
found = prv
break
if found is None:
providers = auth.get_auth_providers(AUTH_PROVIDER_TYPE)
if not providers:
raise ValueError('Legacy API password provider not found')
return await auth.async_get_or_create_user(
await found.async_get_or_create_credentials({})
)
try:
provider = cast(LegacyApiPasswordAuthProvider, providers[0])
provider.async_validate_login(password)
return await auth.async_get_or_create_user(
await provider.async_get_or_create_credentials({})
)
except InvalidAuthError:
return None
@AUTH_PROVIDERS.register('legacy_api_password')
@AUTH_PROVIDERS.register(AUTH_PROVIDER_TYPE)
class LegacyApiPasswordAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords."""
"""An auth provider support legacy api_password."""
DEFAULT_TITLE = 'Legacy API Password'
@property
def api_password(self) -> str:
"""Return api_password."""
return str(self.config[CONF_API_PASSWORD])
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
"""Return a flow to login."""
return LegacyLoginFlow(self)
@callback
def async_validate_login(self, password: str) -> None:
"""Validate a username and password."""
hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
"""Validate password."""
api_password = str(self.config[CONF_API_PASSWORD])
if not hmac.compare_digest(hass_http.api_password.encode('utf-8'),
if not hmac.compare_digest(api_password.encode('utf-8'),
password.encode('utf-8')):
raise InvalidAuthError
@@ -99,12 +100,6 @@ class LegacyLoginFlow(LoginFlow):
"""Handle the step of the form."""
errors = {}
hass_http = getattr(self.hass, 'http', None)
if hass_http is None or not hass_http.api_password:
return self.async_abort(
reason='no_api_password_set'
)
if user_input is not None:
try:
cast(LegacyApiPasswordAuthProvider, self._auth_provider)\

View File

@@ -5,7 +5,7 @@ import os
import sys
from time import time
from collections import OrderedDict
from typing import Any, Optional, Dict
from typing import Any, Optional, Dict, Set
import voluptuous as vol
@@ -28,8 +28,16 @@ ERROR_LOG_FILENAME = 'home-assistant.log'
# hass.data key for logging information.
DATA_LOGGING = 'logging'
FIRST_INIT_COMPONENT = {'system_log', 'recorder', 'mqtt', 'mqtt_eventstream',
'logger', 'introduction', 'frontend', 'history'}
LOGGING_COMPONENT = {'logger', 'system_log'}
FIRST_INIT_COMPONENT = {
'recorder',
'mqtt',
'mqtt_eventstream',
'introduction',
'frontend',
'history',
}
def from_config_dict(config: Dict[str, Any],
@@ -91,12 +99,12 @@ async def async_from_config_dict(config: Dict[str, Any],
"This may cause issues")
core_config = config.get(core.DOMAIN, {})
has_api_password = bool(config.get('http', {}).get('api_password'))
api_password = config.get('http', {}).get('api_password')
trusted_networks = config.get('http', {}).get('trusted_networks')
try:
await conf_util.async_process_ha_core_config(
hass, core_config, has_api_password, trusted_networks)
hass, core_config, api_password, trusted_networks)
except vol.Invalid as config_err:
conf_util.async_log_exception(
config_err, 'homeassistant', core_config, hass)
@@ -117,12 +125,9 @@ async def async_from_config_dict(config: Dict[str, Any],
hass, config, core_config.get(conf_util.CONF_PACKAGES, {}))
hass.config_entries = config_entries.ConfigEntries(hass, config)
await hass.config_entries.async_load()
await hass.config_entries.async_initialize()
# Filter out the repeating and common config section [homeassistant]
components = set(key.split(' ')[0] for key in config.keys()
if key != core.DOMAIN)
components.update(hass.config_entries.async_domains())
components = _get_components(hass, config)
# Resolve all dependencies of all components.
for component in list(components):
@@ -144,17 +149,25 @@ async def async_from_config_dict(config: Dict[str, Any],
_LOGGER.info("Home Assistant core initialized")
# stage 0, load logging components
for component in components:
if component in LOGGING_COMPONENT:
hass.async_create_task(
async_setup_component(hass, component, config))
await hass.async_block_till_done()
# stage 1
for component in components:
if component not in FIRST_INIT_COMPONENT:
continue
hass.async_create_task(async_setup_component(hass, component, config))
if component in FIRST_INIT_COMPONENT:
hass.async_create_task(
async_setup_component(hass, component, config))
await hass.async_block_till_done()
# stage 2
for component in components:
if component in FIRST_INIT_COMPONENT:
if component in FIRST_INIT_COMPONENT or component in LOGGING_COMPONENT:
continue
hass.async_create_task(async_setup_component(hass, component, config))
@@ -375,3 +388,21 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
if lib_dir not in sys.path:
sys.path.insert(0, lib_dir)
return deps_dir
@core.callback
def _get_components(hass: core.HomeAssistant,
config: Dict[str, Any]) -> Set[str]:
"""Get components to set up."""
# Filter out the repeating and common config section [homeassistant]
components = set(key.split(' ')[0] for key in config.keys()
if key != core.DOMAIN)
# Add config entry domains
components.update(hass.config_entries.async_domains()) # type: ignore
# Make sure the Hass.io component is loaded
if 'HASSIO' in os.environ:
components.add('hassio')
return components

View File

@@ -17,7 +17,7 @@ import voluptuous as vol
import homeassistant.core as ha
import homeassistant.config as conf_util
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.service import extract_entity_ids
from homeassistant.helpers.service import async_extract_entity_ids
from homeassistant.helpers import intent
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
@@ -70,7 +70,7 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
"""Set up general services related to Home Assistant."""
async def async_handle_turn_service(service):
"""Handle calls to homeassistant.turn_on/off."""
entity_ids = extract_entity_ids(hass, service)
entity_ids = await async_extract_entity_ids(hass, service)
# Generic turn on/off method requires entity id
if not entity_ids:
@@ -166,6 +166,7 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
_LOGGER.error(err)
return
# auth only processed during startup
await conf_util.async_process_ha_core_config(
hass, conf.get(ha.DOMAIN) or {})

View File

@@ -171,13 +171,12 @@ class AdsHub:
hnotify, huser = self._client.add_device_notification(
name, attr, self._device_notification_callback)
hnotify = int(hnotify)
self._notification_items[hnotify] = NotificationItem(
hnotify, huser, name, plc_datatype, callback)
_LOGGER.debug(
"Added device notification %d for variable %s", hnotify, name)
self._notification_items[hnotify] = NotificationItem(
hnotify, huser, name, plc_datatype, callback)
def _device_notification_callback(self, notification, name):
"""Handle device notifications."""
contents = notification.contents
@@ -187,9 +186,10 @@ class AdsHub:
data = contents.data
try:
notification_item = self._notification_items[hnotify]
with self._lock:
notification_item = self._notification_items[hnotify]
except KeyError:
_LOGGER.debug("Unknown device notification handle: %d", hnotify)
_LOGGER.error("Unknown device notification handle: %d", hnotify)
return
# Parse data to desired datatype

View File

@@ -11,7 +11,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
REQUIREMENTS = ['opensensemap-api==0.1.4']
REQUIREMENTS = ['opensensemap-api==0.1.5']
_LOGGER = logging.getLogger(__name__)

View File

@@ -342,18 +342,18 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
)
@callback
def message_received(topic, payload, qos):
def message_received(msg):
"""Run when new MQTT message has been received."""
if payload == self._payload_disarm:
if msg.payload == self._payload_disarm:
self.async_alarm_disarm(self._code)
elif payload == self._payload_arm_home:
elif msg.payload == self._payload_arm_home:
self.async_alarm_arm_home(self._code)
elif payload == self._payload_arm_away:
elif msg.payload == self._payload_arm_away:
self.async_alarm_arm_away(self._code)
elif payload == self._payload_arm_night:
elif msg.payload == self._payload_arm_night:
self.async_alarm_arm_night(self._code)
else:
_LOGGER.warning("Received unexpected payload: %s", payload)
_LOGGER.warning("Received unexpected payload: %s", msg.payload)
return
await mqtt.async_subscribe(

View File

@@ -18,7 +18,7 @@ from homeassistant.const import (
STATE_ALARM_ARMED_CUSTOM_BYPASS)
REQUIREMENTS = ['total_connect_client==0.22']
REQUIREMENTS = ['total_connect_client==0.25']
_LOGGER = logging.getLogger(__name__)

View File

@@ -131,6 +131,11 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
if code:
self.hass.data[DATA_AD].send("{!s}3".format(code))
def alarm_arm_night(self, code=None):
"""Send arm night command."""
if code:
self.hass.data[DATA_AD].send("{!s}33".format(code))
def alarm_toggle_chime(self, code=None):
"""Send toggle chime command."""
if code:

View File

@@ -89,7 +89,7 @@ async def async_setup(hass, config):
async def async_handle_alert_service(service_call):
"""Handle calls to alert services."""
alert_ids = service.extract_entity_ids(hass, service_call)
alert_ids = await service.async_extract_entity_ids(hass, service_call)
for alert_id in alert_ids:
for alert in entities:

View File

@@ -1,21 +1,21 @@
"""Support for alexa Smart Home Skill API."""
import asyncio
from collections import OrderedDict
from datetime import datetime
import json
import logging
import math
from collections import OrderedDict
from datetime import datetime
from uuid import uuid4
import aiohttp
import async_timeout
import homeassistant.core as ha
import homeassistant.util.color as color_util
from homeassistant.components import (
alert, automation, binary_sensor, cover, fan, group, http,
input_boolean, light, lock, media_player, scene, script, sensor, switch)
from homeassistant.components.climate import const as climate
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.event import async_track_state_change
from homeassistant.const import (
ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CLOUD_NEVER_EXPOSED_ENTITIES,
@@ -25,14 +25,14 @@ from homeassistant.const import (
SERVICE_UNLOCK, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_UP, SERVICE_VOLUME_SET,
SERVICE_VOLUME_MUTE, STATE_LOCKED, STATE_ON, STATE_OFF, STATE_UNAVAILABLE,
STATE_UNLOCKED, TEMP_CELSIUS, TEMP_FAHRENHEIT, MATCH_ALL)
import homeassistant.core as ha
import homeassistant.util.color as color_util
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.event import async_track_state_change
from homeassistant.util.decorator import Registry
from homeassistant.util.temperature import convert as convert_temperature
from .auth import Auth
from .const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_ENDPOINT, \
CONF_ENTITY_CONFIG, CONF_FILTER, DATE_FORMAT, DEFAULT_TIMEOUT
from .auth import Auth
_LOGGER = logging.getLogger(__name__)
@@ -1115,12 +1115,15 @@ class SmartHomeView(http.HomeAssistantView):
the response.
"""
hass = request.app['hass']
user = request[http.KEY_HASS_USER]
message = await request.json()
_LOGGER.debug("Received Alexa Smart Home request: %s", message)
response = await async_handle_message(
hass, self.smart_home_config, message)
hass, self.smart_home_config, message,
context=ha.Context(user_id=user.id)
)
_LOGGER.debug("Sending Alexa Smart Home response: %s", response)
return b'' if response is None else self.json(response)

View File

@@ -0,0 +1,13 @@
{
"config": {
"step": {
"user": {
"data": {
"api_key": "\u0e04\u0e35\u0e22\u0e4c API",
"app_key": "\u0e23\u0e2b\u0e31\u0e2a\u0e41\u0e2d\u0e1b\u0e1e\u0e25\u0e34\u0e40\u0e04\u0e0a\u0e31\u0e19"
},
"title": "\u0e01\u0e23\u0e2d\u0e01\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e02\u0e2d\u0e07\u0e04\u0e38\u0e13"
}
}
}
}

View File

@@ -296,6 +296,7 @@ class AmbientStation:
def __init__(self, hass, config_entry, client, monitored_conditions):
"""Initialize."""
self._config_entry = config_entry
self._entry_setup_complete = False
self._hass = hass
self._watchdog_listener = None
self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
@@ -362,12 +363,18 @@ class AmbientStation:
'name', station['macAddress']),
}
for component in ('binary_sensor', 'sensor'):
self._hass.async_create_task(
self._hass.config_entries.async_forward_entry_setup(
self._config_entry, component))
# If the websocket disconnects and reconnects, the on_subscribed
# handler will get called again; in that case, we don't want to
# attempt forward setup of the config entry (because it will have
# already been done):
if not self._entry_setup_complete:
for component in ('binary_sensor', 'sensor'):
self._hass.async_create_task(
self._hass.config_entries.async_forward_entry_setup(
self._config_entry, component))
self._entry_setup_complete = True
self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
self.client.websocket.on_connect(on_connect)
self.client.websocket.on_data(on_data)

View File

@@ -13,7 +13,7 @@ from homeassistant.const import (
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['amcrest==1.2.3']
REQUIREMENTS = ['amcrest==1.2.5']
DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__)

View File

@@ -1,6 +1,10 @@
"""Support for Amcrest IP cameras."""
import asyncio
import logging
from requests import RequestException
from urllib3.exceptions import ReadTimeoutError
from homeassistant.components.amcrest import (
DATA_AMCREST, STREAM_SOURCE_LIST, TIMEOUT)
from homeassistant.components.camera import Camera
@@ -43,12 +47,20 @@ class AmcrestCam(Camera):
self._stream_source = amcrest.stream_source
self._resolution = amcrest.resolution
self._token = self._auth = amcrest.authentication
self._snapshot_lock = asyncio.Lock()
def camera_image(self):
async def async_camera_image(self):
"""Return a still image response from the camera."""
# Send the request to snap a picture and return raw jpg data
response = self._camera.snapshot(channel=self._resolution)
return response.data
async with self._snapshot_lock:
try:
# Send the request to snap a picture and return raw jpg data
response = await self.hass.async_add_executor_job(
self._camera.snapshot, self._resolution)
return response.data
except (RequestException, ReadTimeoutError, ValueError) as error:
_LOGGER.error(
'Could not get camera image due to error %s', error)
return None
async def handle_async_mjpeg_stream(self, request):
"""Return an MJPEG stream."""
@@ -85,3 +97,8 @@ class AmcrestCam(Camera):
def name(self):
"""Return the name of this camera."""
return self._name
@property
def stream_source(self):
"""Return the source of the stream."""
return self._camera.rtsp_url(typeno=self._resolution)

View File

@@ -0,0 +1,6 @@
"""
Support for functionality to interact with Android TV and Fire TV devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.androidtv/
"""

View File

@@ -0,0 +1,454 @@
"""
Support for functionality to interact with Android TV and Fire TV devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.androidtv/
"""
import functools
import logging
import voluptuous as vol
from homeassistant.components.media_player import (
MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.components.media_player.const import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK,
SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP)
from homeassistant.const import (
ATTR_COMMAND, ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME,
CONF_PORT, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
STATE_STANDBY)
import homeassistant.helpers.config_validation as cv
ANDROIDTV_DOMAIN = 'androidtv'
REQUIREMENTS = ['androidtv==0.0.12']
_LOGGER = logging.getLogger(__name__)
SUPPORT_ANDROIDTV = SUPPORT_PAUSE | SUPPORT_PLAY | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_STOP | SUPPORT_VOLUME_MUTE | \
SUPPORT_VOLUME_STEP
SUPPORT_FIRETV = SUPPORT_PAUSE | SUPPORT_PLAY | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_SELECT_SOURCE | SUPPORT_STOP
CONF_ADBKEY = 'adbkey'
CONF_ADB_SERVER_IP = 'adb_server_ip'
CONF_ADB_SERVER_PORT = 'adb_server_port'
CONF_APPS = 'apps'
CONF_GET_SOURCES = 'get_sources'
DEFAULT_NAME = 'Android TV'
DEFAULT_PORT = 5555
DEFAULT_ADB_SERVER_PORT = 5037
DEFAULT_GET_SOURCES = True
DEFAULT_DEVICE_CLASS = 'auto'
DEVICE_ANDROIDTV = 'androidtv'
DEVICE_FIRETV = 'firetv'
DEVICE_CLASSES = [DEFAULT_DEVICE_CLASS, DEVICE_ANDROIDTV, DEVICE_FIRETV]
SERVICE_ADB_COMMAND = 'adb_command'
SERVICE_ADB_COMMAND_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_COMMAND): cv.string,
})
def has_adb_files(value):
"""Check that ADB key files exist."""
priv_key = value
pub_key = '{}.pub'.format(value)
cv.isfile(pub_key)
return cv.isfile(priv_key)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS):
vol.In(DEVICE_CLASSES),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_ADBKEY): has_adb_files,
vol.Optional(CONF_ADB_SERVER_IP): cv.string,
vol.Optional(CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT):
cv.port,
vol.Optional(CONF_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean,
vol.Optional(CONF_APPS, default=dict()):
vol.Schema({cv.string: cv.string})
})
# Translate from `AndroidTV` / `FireTV` reported state to HA state.
ANDROIDTV_STATES = {'off': STATE_OFF,
'idle': STATE_IDLE,
'standby': STATE_STANDBY,
'playing': STATE_PLAYING,
'paused': STATE_PAUSED}
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Android TV / Fire TV platform."""
from androidtv import setup
hass.data.setdefault(ANDROIDTV_DOMAIN, {})
host = '{0}:{1}'.format(config[CONF_HOST], config[CONF_PORT])
if CONF_ADB_SERVER_IP not in config:
# Use "python-adb" (Python ADB implementation)
if CONF_ADBKEY in config:
aftv = setup(host, config[CONF_ADBKEY],
device_class=config[CONF_DEVICE_CLASS])
adb_log = " using adbkey='{0}'".format(config[CONF_ADBKEY])
else:
aftv = setup(host, device_class=config[CONF_DEVICE_CLASS])
adb_log = ""
else:
# Use "pure-python-adb" (communicate with ADB server)
aftv = setup(host, adb_server_ip=config[CONF_ADB_SERVER_IP],
adb_server_port=config[CONF_ADB_SERVER_PORT],
device_class=config[CONF_DEVICE_CLASS])
adb_log = " using ADB server at {0}:{1}".format(
config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT])
if not aftv.available:
# Determine the name that will be used for the device in the log
if CONF_NAME in config:
device_name = config[CONF_NAME]
elif config[CONF_DEVICE_CLASS] == DEVICE_ANDROIDTV:
device_name = 'Android TV device'
elif config[CONF_DEVICE_CLASS] == DEVICE_FIRETV:
device_name = 'Fire TV device'
else:
device_name = 'Android TV / Fire TV device'
_LOGGER.warning("Could not connect to %s at %s%s",
device_name, host, adb_log)
return
if host in hass.data[ANDROIDTV_DOMAIN]:
_LOGGER.warning("Platform already setup on %s, skipping", host)
else:
if aftv.DEVICE_CLASS == DEVICE_ANDROIDTV:
device = AndroidTVDevice(aftv, config[CONF_NAME],
config[CONF_APPS])
device_name = config[CONF_NAME] if CONF_NAME in config \
else 'Android TV'
else:
device = FireTVDevice(aftv, config[CONF_NAME], config[CONF_APPS],
config[CONF_GET_SOURCES])
device_name = config[CONF_NAME] if CONF_NAME in config \
else 'Fire TV'
add_entities([device])
_LOGGER.debug("Setup %s at %s%s", device_name, host, adb_log)
hass.data[ANDROIDTV_DOMAIN][host] = device
if hass.services.has_service(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND):
return
def service_adb_command(service):
"""Dispatch service calls to target entities."""
cmd = service.data.get(ATTR_COMMAND)
entity_id = service.data.get(ATTR_ENTITY_ID)
target_devices = [dev for dev in hass.data[ANDROIDTV_DOMAIN].values()
if dev.entity_id in entity_id]
for target_device in target_devices:
output = target_device.adb_command(cmd)
# log the output if there is any
if output:
_LOGGER.info("Output of command '%s' from '%s': %s",
cmd, target_device.entity_id, repr(output))
hass.services.register(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND,
service_adb_command,
schema=SERVICE_ADB_COMMAND_SCHEMA)
def adb_decorator(override_available=False):
"""Send an ADB command if the device is available and catch exceptions."""
def _adb_decorator(func):
"""Wait if previous ADB commands haven't finished."""
@functools.wraps(func)
def _adb_exception_catcher(self, *args, **kwargs):
# If the device is unavailable, don't do anything
if not self.available and not override_available:
return None
try:
return func(self, *args, **kwargs)
except self.exceptions as err:
_LOGGER.error(
"Failed to execute an ADB command. ADB connection re-"
"establishing attempt in the next update. Error: %s", err)
self._available = False # pylint: disable=protected-access
return None
return _adb_exception_catcher
return _adb_decorator
class ADBDevice(MediaPlayerDevice):
"""Representation of an Android TV or Fire TV device."""
def __init__(self, aftv, name, apps):
"""Initialize the Android TV / Fire TV device."""
from androidtv.constants import APPS, KEYS
self.aftv = aftv
self._name = name
self._apps = APPS
self._apps.update(apps)
self._keys = KEYS
# ADB exceptions to catch
if not self.aftv.adb_server_ip:
# Using "python-adb" (Python ADB implementation)
from adb.adb_protocol import (InvalidChecksumError,
InvalidCommandError,
InvalidResponseError)
from adb.usb_exceptions import TcpTimeoutException
self.exceptions = (AttributeError, BrokenPipeError, TypeError,
ValueError, InvalidChecksumError,
InvalidCommandError, InvalidResponseError,
TcpTimeoutException)
else:
# Using "pure-python-adb" (communicate with ADB server)
self.exceptions = (ConnectionResetError,)
# Property attributes
self._available = self.aftv.available
self._current_app = None
self._state = None
@property
def app_id(self):
"""Return the current app."""
return self._current_app
@property
def app_name(self):
"""Return the friendly name of the current app."""
return self._apps.get(self._current_app, self._current_app)
@property
def available(self):
"""Return whether or not the ADB connection is valid."""
return self._available
@property
def name(self):
"""Return the device name."""
return self._name
@property
def should_poll(self):
"""Device should be polled."""
return True
@property
def state(self):
"""Return the state of the player."""
return self._state
@adb_decorator()
def media_play(self):
"""Send play command."""
self.aftv.media_play()
@adb_decorator()
def media_pause(self):
"""Send pause command."""
self.aftv.media_pause()
@adb_decorator()
def media_play_pause(self):
"""Send play/pause command."""
self.aftv.media_play_pause()
@adb_decorator()
def turn_on(self):
"""Turn on the device."""
self.aftv.turn_on()
@adb_decorator()
def turn_off(self):
"""Turn off the device."""
self.aftv.turn_off()
@adb_decorator()
def media_previous_track(self):
"""Send previous track command (results in rewind)."""
self.aftv.media_previous()
@adb_decorator()
def media_next_track(self):
"""Send next track command (results in fast-forward)."""
self.aftv.media_next()
@adb_decorator()
def adb_command(self, cmd):
"""Send an ADB command to an Android TV / Fire TV device."""
key = self._keys.get(cmd)
if key:
return self.aftv.adb_shell('input keyevent {}'.format(key))
if cmd == 'GET_PROPERTIES':
return self.aftv.get_properties_dict()
return self.aftv.adb_shell(cmd)
class AndroidTVDevice(ADBDevice):
"""Representation of an Android TV device."""
def __init__(self, aftv, name, apps):
"""Initialize the Android TV device."""
super().__init__(aftv, name, apps)
self._device = None
self._muted = None
self._device_properties = self.aftv.device_properties
self._unique_id = 'androidtv-{}-{}'.format(
name, self._device_properties['serialno'])
self._volume = None
@adb_decorator(override_available=True)
def update(self):
"""Update the device state and, if necessary, re-connect."""
# Check if device is disconnected.
if not self._available:
# Try to connect
self._available = self.aftv.connect(always_log_errors=False)
# To be safe, wait until the next update to run ADB commands.
return
# If the ADB connection is not intact, don't update.
if not self._available:
return
# Get the `state`, `current_app`, and `running_apps`.
state, self._current_app, self._device, self._muted, self._volume = \
self.aftv.update()
self._state = ANDROIDTV_STATES[state]
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._muted
@property
def source(self):
"""Return the current playback device."""
return self._device
@property
def supported_features(self):
"""Flag media player features that are supported."""
return SUPPORT_ANDROIDTV
@property
def unique_id(self):
"""Return the device unique id."""
return self._unique_id
@property
def volume_level(self):
"""Return the volume level."""
return self._volume
@adb_decorator()
def media_stop(self):
"""Send stop command."""
self.aftv.media_stop()
@adb_decorator()
def mute_volume(self, mute):
"""Mute the volume."""
self.aftv.mute_volume()
@adb_decorator()
def volume_down(self):
"""Send volume down command."""
self.aftv.volume_down()
@adb_decorator()
def volume_up(self):
"""Send volume up command."""
self.aftv.volume_up()
class FireTVDevice(ADBDevice):
"""Representation of a Fire TV device."""
def __init__(self, aftv, name, apps, get_sources):
"""Initialize the Fire TV device."""
super().__init__(aftv, name, apps)
self._get_sources = get_sources
self._running_apps = None
@adb_decorator(override_available=True)
def update(self):
"""Update the device state and, if necessary, re-connect."""
# Check if device is disconnected.
if not self._available:
# Try to connect
self._available = self.aftv.connect(always_log_errors=False)
# To be safe, wait until the next update to run ADB commands.
return
# If the ADB connection is not intact, don't update.
if not self._available:
return
# Get the `state`, `current_app`, and `running_apps`.
state, self._current_app, self._running_apps = \
self.aftv.update(self._get_sources)
self._state = ANDROIDTV_STATES[state]
@property
def source(self):
"""Return the current app."""
return self._current_app
@property
def source_list(self):
"""Return a list of running apps."""
return self._running_apps
@property
def supported_features(self):
"""Flag media player features that are supported."""
return SUPPORT_FIRETV
@adb_decorator()
def media_stop(self):
"""Send stop (back) command."""
self.aftv.back()
@adb_decorator()
def select_source(self, source):
"""Select input source.
If the source starts with a '!', then it will close the app instead of
opening it.
"""
if isinstance(source, str):
if not source.startswith('!'):
self.aftv.launch_app(source)
else:
self.aftv.stop_app(source[1:].lstrip())

View File

@@ -0,0 +1,11 @@
# Describes the format for available Android TV and Fire TV services
adb_command:
description: Send an ADB command to an Android TV / Fire TV device.
fields:
entity_id:
description: Name(s) of Android TV / Fire TV entities.
example: 'media_player.android_tv_living_room'
command:
description: Either a key command or an ADB shell command.
example: 'HOME'

View File

@@ -6,7 +6,7 @@ import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
from homeassistant.components import apcupsd
from homeassistant.const import (TEMP_CELSIUS, CONF_RESOURCES)
from homeassistant.const import (TEMP_CELSIUS, CONF_RESOURCES, POWER_WATT)
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -57,7 +57,7 @@ SENSOR_TYPES = {
'nombattv': ['Battery Nominal Voltage', 'V', 'mdi:flash'],
'nominv': ['Nominal Input Voltage', 'V', 'mdi:flash'],
'nomoutv': ['Nominal Output Voltage', 'V', 'mdi:flash'],
'nompower': ['Nominal Output Power', 'W', 'mdi:flash'],
'nompower': ['Nominal Output Power', POWER_WATT, 'mdi:flash'],
'nomapnt': ['Nominal Apparent Power', 'VA', 'mdi:flash'],
'numxfers': ['Transfer Count', '', 'mdi:counter'],
'outcurnt': ['Output Current', 'A', 'mdi:flash'],
@@ -93,7 +93,7 @@ INFERRED_UNITS = {
' Volts': 'V',
' Ampere': 'A',
' Volt-Ampere': 'VA',
' Watts': 'W',
' Watts': POWER_WATT,
' Hz': 'Hz',
' C': TEMP_CELSIUS,
' Percent Load Capacity': '%',

View File

@@ -168,11 +168,11 @@ class APIDiscoveryView(HomeAssistantView):
def get(self, request):
"""Get discovery information."""
hass = request.app['hass']
needs_auth = hass.config.api.api_password is not None
return self.json({
ATTR_BASE_URL: hass.config.api.base_url,
ATTR_LOCATION_NAME: hass.config.location_name,
ATTR_REQUIRES_API_PASSWORD: needs_auth,
# always needs authentication
ATTR_REQUIRES_API_PASSWORD: True,
ATTR_VERSION: __version__,
})

View File

@@ -9,7 +9,7 @@ from homeassistant.const import (
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
REQUIREMENTS = ['aioasuswrt==1.1.20']
REQUIREMENTS = ['aioasuswrt==1.1.21']
_LOGGER = logging.getLogger(__name__)

View File

@@ -0,0 +1,11 @@
{
"mfa_setup": {
"notify": {
"step": {
"setup": {
"title": "\u0e15\u0e23\u0e27\u0e08\u0e2a\u0e2d\u0e1a\u0e01\u0e32\u0e23\u0e15\u0e34\u0e14\u0e15\u0e31\u0e49\u0e07"
}
}
}
}
}

View File

@@ -127,6 +127,7 @@ import voluptuous as vol
from homeassistant.auth.models import User, Credentials, \
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
from homeassistant.loader import bind_hass
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_REAL_IP
from homeassistant.components.http.auth import async_sign_path
@@ -184,10 +185,18 @@ RESULT_TYPE_USER = 'user'
_LOGGER = logging.getLogger(__name__)
@bind_hass
def create_auth_code(hass, client_id: str, user: User) -> str:
"""Create an authorization code to fetch tokens."""
return hass.data[DOMAIN](client_id, user)
async def async_setup(hass, config):
"""Component to allow users to login."""
store_result, retrieve_result = _create_auth_code_store()
hass.data[DOMAIN] = store_result
hass.http.register_view(TokenView(retrieve_result))
hass.http.register_view(LinkUserView(retrieve_result))
@@ -450,6 +459,7 @@ async def websocket_current_user(
'id': user.id,
'name': user.name,
'is_owner': user.is_owner,
'is_admin': user.is_admin,
'credentials': [{'auth_provider_type': c.auth_provider_type,
'auth_provider_id': c.auth_provider_id}
for c in user.credentials],

View File

@@ -1,4 +1,5 @@
"""Helpers to resolve client ID/secret."""
import logging
import asyncio
from ipaddress import ip_address
from html.parser import HTMLParser
@@ -9,6 +10,8 @@ from aiohttp.client_exceptions import ClientError
from homeassistant.util.network import is_local
_LOGGER = logging.getLogger(__name__)
async def verify_redirect_uri(hass, client_id, redirect_uri):
"""Verify that the client and redirect uri match."""
@@ -78,7 +81,8 @@ async def fetch_redirect_uris(hass, url):
if chunks == 10:
break
except (asyncio.TimeoutError, ClientError):
except (asyncio.TimeoutError, ClientError) as ex:
_LOGGER.error("Error while looking up redirect_uri %s: %s", url, ex)
pass
# Authorization endpoints verifying that a redirect_uri is allowed for use

View File

@@ -7,7 +7,7 @@ import logging
import voluptuous as vol
from homeassistant.setup import async_prepare_setup_platform
from homeassistant.core import CoreState
from homeassistant.core import CoreState, Context
from homeassistant.loader import bind_hass
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
@@ -120,7 +120,7 @@ async def async_setup(hass, config):
async def trigger_service_handler(service_call):
"""Handle automation triggers."""
tasks = []
for entity in component.async_extract_from_service(service_call):
for entity in await component.async_extract_from_service(service_call):
tasks.append(entity.async_trigger(
service_call.data.get(ATTR_VARIABLES),
skip_condition=True,
@@ -133,7 +133,7 @@ async def async_setup(hass, config):
"""Handle automation turn on/off service calls."""
tasks = []
method = 'async_{}'.format(service_call.service)
for entity in component.async_extract_from_service(service_call):
for entity in await component.async_extract_from_service(service_call):
tasks.append(getattr(entity, method)())
if tasks:
@@ -142,7 +142,7 @@ async def async_setup(hass, config):
async def toggle_service_handler(service_call):
"""Handle automation toggle service calls."""
tasks = []
for entity in component.async_extract_from_service(service_call):
for entity in await component.async_extract_from_service(service_call):
if entity.is_on:
tasks.append(entity.async_turn_off())
else:
@@ -280,15 +280,21 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
This method is a coroutine.
"""
if skip_condition or self._cond_func(variables):
self.async_set_context(context)
self.hass.bus.async_fire(EVENT_AUTOMATION_TRIGGERED, {
ATTR_NAME: self._name,
ATTR_ENTITY_ID: self.entity_id,
}, context=context)
await self._async_action(self.entity_id, variables, context)
self._last_triggered = utcnow()
await self.async_update_ha_state()
if not skip_condition and not self._cond_func(variables):
return
# Create a new context referring to the old context.
parent_id = None if context is None else context.id
trigger_context = Context(parent_id=parent_id)
self.async_set_context(trigger_context)
self.hass.bus.async_fire(EVENT_AUTOMATION_TRIGGERED, {
ATTR_NAME: self._name,
ATTR_ENTITY_ID: self.entity_id,
}, context=trigger_context)
await self._async_action(self.entity_id, variables, trigger_context)
self._last_triggered = utcnow()
await self.async_update_ha_state()
async def async_will_remove_from_hass(self):
"""Remove listeners when removing automation from HASS."""

View File

@@ -29,18 +29,18 @@ async def async_trigger(hass, config, action, automation_info):
encoding = config[CONF_ENCODING] or None
@callback
def mqtt_automation_listener(msg_topic, msg_payload, qos):
def mqtt_automation_listener(mqttmsg):
"""Listen for MQTT messages."""
if payload is None or payload == msg_payload:
if payload is None or payload == mqttmsg.payload:
data = {
'platform': 'mqtt',
'topic': msg_topic,
'payload': msg_payload,
'qos': qos,
'topic': mqttmsg.topic,
'payload': mqttmsg.payload,
'qos': mqttmsg.qos,
}
try:
data['payload_json'] = json.loads(msg_payload)
data['payload_json'] = json.loads(mqttmsg.payload)
except ValueError:
pass

View File

@@ -119,6 +119,17 @@ class TodSensor(BinarySensorDevice):
self.hass.config.time_zone).isoformat(),
}
def _naive_time_to_utc_datetime(self, naive_time):
"""Convert naive time from config to utc_datetime with current day."""
# get the current local date from utc time
current_local_date = self.current_datetime.astimezone(
self.hass.config.time_zone).date()
# calcuate utc datetime corecponding to local time
utc_datetime = self.hass.config.time_zone.localize(
datetime.combine(
current_local_date, naive_time)).astimezone(tz=pytz.UTC)
return utc_datetime
def _calculate_initial_boudary_time(self):
"""Calculate internal absolute time boudaries."""
nowutc = self.current_datetime
@@ -134,9 +145,7 @@ class TodSensor(BinarySensorDevice):
# datetime.combine(date, time, tzinfo) is not supported
# in python 3.5. The self._after is provided
# with hass configured TZ not system wide
after_event_date = datetime.combine(
nowutc, self._after.replace(
tzinfo=self.hass.config.time_zone)).astimezone(tz=pytz.UTC)
after_event_date = self._naive_time_to_utc_datetime(self._after)
self._time_after = after_event_date
@@ -154,9 +163,7 @@ class TodSensor(BinarySensorDevice):
self.hass, self._before, after_event_date)
else:
# Convert local time provided to UTC today, see above
before_event_date = datetime.combine(
nowutc, self._before.replace(
tzinfo=self.hass.config.time_zone)).astimezone(tz=pytz.UTC)
before_event_date = self._naive_time_to_utc_datetime(self._before)
# It is safe to add timedelta days=1 to UTC as there is no DST
if before_event_date < after_event_date + self._after_offset:
@@ -190,7 +197,6 @@ class TodSensor(BinarySensorDevice):
async def async_added_to_hass(self):
"""Call when entity about to be added to Home Assistant."""
await super().async_added_to_hass()
self._calculate_initial_boudary_time()
self._calculate_next_update()
self._point_in_time_listener(dt_util.now())

View File

@@ -17,7 +17,7 @@ from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.event import async_track_state_change
from homeassistant.util import utcnow
REQUIREMENTS = ['numpy==1.16.1']
REQUIREMENTS = ['numpy==1.16.2']
_LOGGER = logging.getLogger(__name__)

View File

@@ -42,6 +42,7 @@ CONF_PROVINCE = 'province'
CONF_WORKDAYS = 'workdays'
CONF_EXCLUDES = 'excludes'
CONF_OFFSET = 'days_offset'
CONF_ADD_HOLIDAYS = 'add_holidays'
# By default, Monday - Friday are workdays
DEFAULT_WORKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri']
@@ -59,6 +60,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PROVINCE): cv.string,
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
vol.Optional(CONF_ADD_HOLIDAYS): vol.All(cv.ensure_list, [cv.string]),
})
@@ -72,6 +74,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
workdays = config.get(CONF_WORKDAYS)
excludes = config.get(CONF_EXCLUDES)
days_offset = config.get(CONF_OFFSET)
add_holidays = config.get(CONF_ADD_HOLIDAYS)
year = (get_date(datetime.today()) + timedelta(days=days_offset)).year
obj_holidays = getattr(holidays, country)(years=year)
@@ -92,6 +95,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
province, country)
return
# Add custom holidays
try:
obj_holidays.append(add_holidays)
except TypeError:
_LOGGER.debug("No custom holidays or invalid holidays")
_LOGGER.debug("Found the following holidays for your configuration:")
for date, name in sorted(obj_holidays.items()):
_LOGGER.debug("%s %s", date, name)

View File

@@ -10,7 +10,7 @@ from homeassistant.const import (
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
REQUIREMENTS = ['blinkpy==0.12.1']
REQUIREMENTS = ['blinkpy==0.13.1']
_LOGGER = logging.getLogger(__name__)
@@ -44,7 +44,7 @@ BINARY_SENSORS = {
SENSORS = {
TYPE_TEMPERATURE: ['Temperature', TEMP_FAHRENHEIT, 'mdi:thermometer'],
TYPE_BATTERY: ['Battery', '%', 'mdi:battery-80'],
TYPE_WIFI_STRENGTH: ['Wifi Signal', 'bars', 'mdi:wifi-strength-2'],
TYPE_WIFI_STRENGTH: ['Wifi Signal', 'dBm', 'mdi:wifi-strength-2'],
}
BINARY_SENSOR_SCHEMA = vol.Schema({

View File

@@ -28,6 +28,12 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import ( # noqa
PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE)
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
from homeassistant.components.media_player.const import (
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE,
SERVICE_PLAY_MEDIA, DOMAIN as DOMAIN_MP)
from homeassistant.components.stream import request_stream
from homeassistant.components.stream.const import (
OUTPUT_FORMATS, FORMAT_CONTENT_TYPE)
from homeassistant.components import websocket_api
import homeassistant.helpers.config_validation as cv
@@ -39,11 +45,14 @@ _LOGGER = logging.getLogger(__name__)
SERVICE_ENABLE_MOTION = 'enable_motion_detection'
SERVICE_DISABLE_MOTION = 'disable_motion_detection'
SERVICE_SNAPSHOT = 'snapshot'
SERVICE_PLAY_STREAM = 'play_stream'
SCAN_INTERVAL = timedelta(seconds=30)
ENTITY_ID_FORMAT = DOMAIN + '.{}'
ATTR_FILENAME = 'filename'
ATTR_MEDIA_PLAYER = 'media_player'
ATTR_FORMAT = 'format'
STATE_RECORDING = 'recording'
STATE_STREAMING = 'streaming'
@@ -69,6 +78,11 @@ CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(ATTR_FILENAME): cv.template
})
CAMERA_SERVICE_PLAY_STREAM = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(ATTR_MEDIA_PLAYER): cv.entities_domain(DOMAIN_MP),
vol.Optional(ATTR_FORMAT, default='hls'): vol.In(OUTPUT_FORMATS),
})
WS_TYPE_CAMERA_THUMBNAIL = 'camera_thumbnail'
SCHEMA_WS_CAMERA_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_CAMERA_THUMBNAIL,
@@ -176,6 +190,7 @@ async def async_setup(hass, config):
WS_TYPE_CAMERA_THUMBNAIL, websocket_camera_thumbnail,
SCHEMA_WS_CAMERA_THUMBNAIL
)
hass.components.websocket_api.async_register_command(ws_camera_stream)
await component.async_setup(config)
@@ -209,6 +224,10 @@ async def async_setup(hass, config):
SERVICE_SNAPSHOT, CAMERA_SERVICE_SNAPSHOT,
async_handle_snapshot_service
)
component.async_register_entity_service(
SERVICE_PLAY_STREAM, CAMERA_SERVICE_PLAY_STREAM,
async_handle_play_stream_service
)
return True
@@ -273,6 +292,11 @@ class Camera(Entity):
"""Return the interval between frames of the mjpeg stream."""
return 0.5
@property
def stream_source(self):
"""Return the source of the stream."""
return None
def camera_image(self):
"""Return bytes of camera image."""
raise NotImplementedError()
@@ -473,6 +497,33 @@ async def websocket_camera_thumbnail(hass, connection, msg):
msg['id'], 'image_fetch_failed', 'Unable to fetch image'))
@websocket_api.async_response
@websocket_api.websocket_command({
vol.Required('type'): 'camera/stream',
vol.Required('entity_id'): cv.entity_id,
vol.Optional('format', default='hls'): vol.In(OUTPUT_FORMATS),
})
async def ws_camera_stream(hass, connection, msg):
"""Handle get camera stream websocket command.
Async friendly.
"""
try:
camera = _get_camera_from_entity_id(hass, msg['entity_id'])
if not camera.stream_source:
raise HomeAssistantError("{} does not support play stream service"
.format(camera.entity_id))
fmt = msg['format']
url = request_stream(hass, camera.stream_source, fmt=fmt)
connection.send_result(msg['id'], {'url': url})
except HomeAssistantError as ex:
_LOGGER.error(ex)
connection.send_error(
msg['id'], 'start_stream_failed', str(ex))
async def async_handle_snapshot_service(camera, service):
"""Handle snapshot services calls."""
hass = camera.hass
@@ -500,3 +551,25 @@ async def async_handle_snapshot_service(camera, service):
_write_image, snapshot_file, image)
except OSError as err:
_LOGGER.error("Can't write image to file: %s", err)
async def async_handle_play_stream_service(camera, service_call):
"""Handle play stream services calls."""
if not camera.stream_source:
raise HomeAssistantError("{} does not support play stream service"
.format(camera.entity_id))
hass = camera.hass
fmt = service_call.data[ATTR_FORMAT]
entity_ids = service_call.data[ATTR_MEDIA_PLAYER]
url = request_stream(hass, camera.stream_source, fmt=fmt)
data = {
ATTR_ENTITY_ID: entity_ids,
ATTR_MEDIA_CONTENT_ID: "{}{}".format(hass.config.api.base_url, url),
ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt]
}
await hass.services.async_call(
DOMAIN_MP, SERVICE_PLAY_MEDIA, data,
blocking=True, context=service_call.context)

View File

@@ -28,12 +28,14 @@ _LOGGER = logging.getLogger(__name__)
CONF_CONTENT_TYPE = 'content_type'
CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change'
CONF_STILL_IMAGE_URL = 'still_image_url'
CONF_STREAM_SOURCE = 'stream_source'
CONF_FRAMERATE = 'framerate'
DEFAULT_NAME = 'Generic Camera'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_STILL_IMAGE_URL): cv.template,
vol.Optional(CONF_STREAM_SOURCE, default=None): vol.Any(None, cv.string),
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]),
vol.Optional(CONF_LIMIT_REFETCH_TO_URL_CHANGE, default=False): cv.boolean,
@@ -62,6 +64,7 @@ class GenericCamera(Camera):
self._authentication = device_info.get(CONF_AUTHENTICATION)
self._name = device_info.get(CONF_NAME)
self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
self._stream_source = device_info[CONF_STREAM_SOURCE]
self._still_image_url.hass = hass
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
self._frame_interval = 1 / device_info[CONF_FRAMERATE]
@@ -128,7 +131,7 @@ class GenericCamera(Camera):
url, auth=self._auth)
self._last_image = await response.read()
except asyncio.TimeoutError:
_LOGGER.error("Timeout getting camera image")
_LOGGER.error("Timeout getting image from: %s", self._name)
return self._last_image
except aiohttp.ClientError as err:
_LOGGER.error("Error getting new camera image: %s", err)
@@ -141,3 +144,8 @@ class GenericCamera(Camera):
def name(self):
"""Return the name of this device."""
return self._name
@property
def stream_source(self):
"""Return the source of the stream."""
return self._stream_source

View File

@@ -11,13 +11,11 @@ from datetime import timedelta
import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE, \
HTTP_HEADER_HA_AUTH
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.util.async_ import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util
from homeassistant.components.camera import async_get_still_stream
REQUIREMENTS = ['pillow==5.4.1']
@@ -209,9 +207,6 @@ class ProxyCamera(Camera):
or config.get(CONF_CACHE_IMAGES))
self._last_image_time = dt_util.utc_from_timestamp(0)
self._last_image = None
self._headers = (
{HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password}
if self.hass.config.api.api_password is not None else None)
self._mode = config.get(CONF_MODE)
def camera_image(self):
@@ -252,7 +247,7 @@ class ProxyCamera(Camera):
return await self.hass.components.camera.async_get_mjpeg_stream(
request, self._proxied_camera)
return await async_get_still_stream(
return await self.hass.components.camera.async_get_still_stream(
request, self._async_stream_image,
self.content_type, self.frame_interval)

View File

@@ -38,6 +38,19 @@ snapshot:
description: Template of a Filename. Variable is entity_id.
example: '/tmp/snapshot_{{ entity_id }}'
play_stream:
description: Play camera stream on supported media player.
fields:
entity_id:
description: Name(s) of entities to stream from.
example: 'camera.living_room_camera'
media_player:
description: Name(s) of media player to stream to.
example: 'media_player.living_room_tv'
format:
description: (Optional) Stream format supported by media player.
example: 'hls'
local_file_update_file_path:
description: Update the file_path for a local_file camera.
fields:

View File

@@ -13,7 +13,7 @@ from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME)
from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['pyxeoma==1.4.0']
REQUIREMENTS = ['pyxeoma==1.4.1']
_LOGGER = logging.getLogger(__name__)

View File

@@ -2,7 +2,7 @@
from homeassistant import config_entries
from homeassistant.helpers import config_entry_flow
REQUIREMENTS = ['pychromecast==2.5.2']
REQUIREMENTS = ['pychromecast==3.0.0']
DOMAIN = 'cast'

View File

@@ -257,6 +257,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
_async_setup_platform(hass, cfg, async_add_entities, None)
for cfg in config])
if any([task.exception() for task in done]):
exceptions = [task.exception() for task in done]
for exception in exceptions:
_LOGGER.debug("Failed to setup chromecast", exc_info=exception)
raise PlatformNotReady
@@ -289,7 +292,7 @@ async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType,
if cast_device is not None:
async_add_entities([cast_device])
remove_handler = async_dispatcher_connect(
async_dispatcher_connect(
hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered)
# Re-play the callback for all past chromecasts, store the objects in
# a list to avoid concurrent modification resulting in exception.
@@ -306,8 +309,6 @@ async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType,
if info.friendly_name is None:
_LOGGER.debug("Cannot retrieve detail information for chromecast"
" %s, the device may not be online", info)
remove_handler()
raise PlatformNotReady
hass.async_add_job(_discover_chromecast, hass, info)
@@ -477,16 +478,10 @@ class CastDevice(MediaPlayerDevice):
))
self._chromecast = chromecast
self._status_listener = CastStatusListener(self, chromecast)
# Initialise connection status as connected because we can only
# register the connection listener *after* the initial connection
# attempt. If the initial connection failed, we would never reach
# this code anyway.
self._available = True
self._available = False
self.cast_status = chromecast.status
self.media_status = chromecast.media_controller.status
_LOGGER.debug("[%s %s (%s:%s)] Connection successful!",
self.entity_id, self._cast_info.friendly_name,
self._cast_info.host, self._cast_info.port)
self._chromecast.start()
self.async_schedule_update_ha_state()
async def async_del_cast_info(self, cast_info):
@@ -562,6 +557,10 @@ class CastDevice(MediaPlayerDevice):
self.entity_id, self._cast_info.friendly_name,
self._cast_info.host, self._cast_info.port,
connection_status.status)
info = self._cast_info
if info.friendly_name is None and not info.is_audio_group:
# We couldn't find friendly_name when the cast was added, retry
self._cast_info = _fill_out_missing_chromecast_info(info)
self._available = new_available
self.schedule_update_ha_state()

View File

@@ -0,0 +1 @@
"""Component to embed Cisco Mobility Express."""

View File

@@ -0,0 +1,83 @@
"""Support for Cisco Mobility Express."""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import (
CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_SSL, CONF_VERIFY_SSL)
REQUIREMENTS = ['ciscomobilityexpress==0.1.2']
_LOGGER = logging.getLogger(__name__)
DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = True
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
})
def get_scanner(hass, config):
"""Validate the configuration and return a Cisco ME scanner."""
from ciscomobilityexpress.ciscome import CiscoMobilityExpress
config = config[DOMAIN]
controller = CiscoMobilityExpress(
config[CONF_HOST],
config[CONF_USERNAME],
config[CONF_PASSWORD],
config.get(CONF_SSL),
config.get(CONF_VERIFY_SSL))
if not controller.is_logged_in():
return None
return CiscoMEDeviceScanner(controller)
class CiscoMEDeviceScanner(DeviceScanner):
"""This class scans for devices associated to a Cisco ME controller."""
def __init__(self, controller):
"""Initialize the scanner."""
self.controller = controller
self.last_results = {}
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return [device.macaddr for device in self.last_results]
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
name = next((
result.clId for result in self.last_results
if result.macaddr == device), None)
return name
def get_extra_attributes(self, device):
"""
Get extra attributes of a device.
Some known extra attributes that may be returned in the device tuple
include SSID, PT (eg 802.11ac), devtype (eg iPhone 7) among others.
"""
device = next((
result for result in self.last_results
if result.macaddr == device), None)
return device._asdict()
def _update_info(self):
"""Check the Cisco ME controller for devices."""
self.last_results = self.controller.get_associated_devices()
_LOGGER.debug("Cisco Mobility Express controller returned:"
" %s", self.last_results)

View File

@@ -1,4 +1,4 @@
"""Proides the constants needed for component."""
"""Provides the constants needed for component."""
ATTR_AUX_HEAT = 'aux_heat'
ATTR_AWAY_MODE = 'away_mode'

View File

@@ -117,7 +117,8 @@ class EphEmberThermostat(ClimateDevice):
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
mode = self._ember.get_zone_mode(self._zone_name)
from pyephember.pyephember import ZoneMode
mode = ZoneMode(self._zone['mode'])
return self.map_mode_eph_hass(mode)
@property

View File

@@ -184,7 +184,8 @@ class EQ3BTSmartThermostat(ClimateDevice):
def update(self):
"""Update the data from the thermostat."""
from bluepy.btle import BTLEException # pylint: disable=import-error
# pylint: disable=import-error,no-name-in-module
from bluepy.btle import BTLEException
try:
self._thermostat.update()
except BTLEException as ex:

View File

@@ -273,6 +273,11 @@ class HoneywellUSThermostat(ClimateDevice):
"""Return the current temperature."""
return self._device.current_temperature
@property
def current_humidity(self):
"""Return the current humidity."""
return self._device.current_humidity
@property
def target_temperature(self):
"""Return the temperature we try to reach."""

View File

@@ -1,43 +1,39 @@
"""Component to integrate the Home Assistant cloud."""
from datetime import datetime, timedelta
import json
import logging
import os
import voluptuous as vol
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, CLOUD_NEVER_EXPOSED_ENTITIES, CONF_REGION,
CONF_MODE, CONF_NAME)
from homeassistant.helpers import entityfilter, config_validation as cv
from homeassistant.util import dt as dt_util
from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import helpers as ga_h
from homeassistant.components.google_assistant import const as ga_c
from homeassistant.const import (
CONF_MODE, CONF_NAME, CONF_REGION, EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entityfilter
from homeassistant.loader import bind_hass
from homeassistant.util.aiohttp import MockRequest
from . import http_api, iot, auth_api, prefs, cloudhooks
from .const import CONFIG_DIR, DOMAIN, SERVERS
from . import http_api
from .const import (
CONF_ACME_DIRECTORY_SERVER, CONF_ALEXA, CONF_ALIASES,
CONF_CLOUDHOOK_CREATE_URL, CONF_COGNITO_CLIENT_ID, CONF_ENTITY_CONFIG,
CONF_FILTER, CONF_GOOGLE_ACTIONS, CONF_GOOGLE_ACTIONS_SYNC_URL,
CONF_RELAYER, CONF_REMOTE_API_URL, CONF_SUBSCRIPTION_INFO_URL,
CONF_USER_POOL_ID, DOMAIN, MODE_DEV, MODE_PROD)
from .prefs import CloudPreferences
REQUIREMENTS = ['warrant==0.6.1']
REQUIREMENTS = ['hass-nabucasa==0.10']
DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__)
CONF_ALEXA = 'alexa'
CONF_ALIASES = 'aliases'
CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
CONF_ENTITY_CONFIG = 'entity_config'
CONF_FILTER = 'filter'
CONF_GOOGLE_ACTIONS = 'google_actions'
CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id'
CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url'
CONF_SUBSCRIPTION_INFO_URL = 'subscription_info_url'
CONF_CLOUDHOOK_CREATE_URL = 'cloudhook_create_url'
DEFAULT_MODE = MODE_PROD
DEFAULT_MODE = 'production'
DEPENDENCIES = ['http']
SERVICE_REMOTE_CONNECT = 'remote_connect'
SERVICE_REMOTE_DISCONNECT = 'remote_disconnect'
MODE_DEV = 'development'
ALEXA_ENTITY_SCHEMA = vol.Schema({
vol.Optional(alexa_sh.CONF_DESCRIPTION): cv.string,
@@ -52,7 +48,7 @@ GOOGLE_ENTITY_SCHEMA = vol.Schema({
})
ASSISTANT_SCHEMA = vol.Schema({
vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA,
vol.Optional(CONF_FILTER, default=dict): entityfilter.FILTER_SCHEMA,
})
ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend({
@@ -63,205 +59,140 @@ GACTIONS_SCHEMA = ASSISTANT_SCHEMA.extend({
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: GOOGLE_ENTITY_SCHEMA},
})
# pylint: disable=no-value-for-parameter
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
vol.In([MODE_DEV] + list(SERVERS)),
vol.In([MODE_DEV, MODE_PROD]),
# Change to optional when we include real servers
vol.Optional(CONF_COGNITO_CLIENT_ID): str,
vol.Optional(CONF_USER_POOL_ID): str,
vol.Optional(CONF_REGION): str,
vol.Optional(CONF_RELAYER): str,
vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str,
vol.Optional(CONF_SUBSCRIPTION_INFO_URL): str,
vol.Optional(CONF_CLOUDHOOK_CREATE_URL): str,
vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): vol.Url(),
vol.Optional(CONF_SUBSCRIPTION_INFO_URL): vol.Url(),
vol.Optional(CONF_CLOUDHOOK_CREATE_URL): vol.Url(),
vol.Optional(CONF_REMOTE_API_URL): vol.Url(),
vol.Optional(CONF_ACME_DIRECTORY_SERVER): vol.Url(),
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
}),
}, extra=vol.ALLOW_EXTRA)
class CloudNotAvailable(HomeAssistantError):
"""Raised when an action requires the cloud but it's not available."""
@bind_hass
@callback
def async_is_logged_in(hass) -> bool:
"""Test if user is logged in."""
return DOMAIN in hass.data and hass.data[DOMAIN].is_logged_in
@bind_hass
@callback
def async_active_subscription(hass) -> bool:
"""Test if user has an active subscription."""
return \
async_is_logged_in(hass) and not hass.data[DOMAIN].subscription_expired
@bind_hass
async def async_create_cloudhook(hass, webhook_id: str) -> str:
"""Create a cloudhook."""
if not async_is_logged_in(hass):
raise CloudNotAvailable
hook = await hass.data[DOMAIN].cloudhooks.async_create(webhook_id, True)
return hook['cloudhook_url']
@bind_hass
async def async_delete_cloudhook(hass, webhook_id: str) -> None:
"""Delete a cloudhook."""
if DOMAIN not in hass.data:
raise CloudNotAvailable
await hass.data[DOMAIN].cloudhooks.async_delete(webhook_id)
@bind_hass
@callback
def async_remote_ui_url(hass) -> str:
"""Get the remote UI URL."""
if not async_is_logged_in(hass):
raise CloudNotAvailable
return "https://" + hass.data[DOMAIN].remote.instance_domain
def is_cloudhook_request(request):
"""Test if a request came from a cloudhook.
Async friendly.
"""
return isinstance(request, MockRequest)
async def async_setup(hass, config):
"""Initialize the Home Assistant cloud."""
from hass_nabucasa import Cloud
from .client import CloudClient
# Process configs
if DOMAIN in config:
kwargs = dict(config[DOMAIN])
else:
kwargs = {CONF_MODE: DEFAULT_MODE}
# Alexa/Google custom config
alexa_conf = kwargs.pop(CONF_ALEXA, None) or ALEXA_SCHEMA({})
google_conf = kwargs.pop(CONF_GOOGLE_ACTIONS, None) or GACTIONS_SCHEMA({})
if CONF_GOOGLE_ACTIONS not in kwargs:
kwargs[CONF_GOOGLE_ACTIONS] = GACTIONS_SCHEMA({})
# Cloud settings
prefs = CloudPreferences(hass)
await prefs.async_initialize()
kwargs[CONF_ALEXA] = alexa_sh.Config(
endpoint=None,
async_get_access_token=None,
should_expose=alexa_conf[CONF_FILTER],
entity_config=alexa_conf.get(CONF_ENTITY_CONFIG),
)
# Cloud user
if not prefs.cloud_user:
user = await hass.auth.async_create_system_user(
'Home Assistant Cloud', [GROUP_ID_ADMIN])
await prefs.async_update(cloud_user=user.id)
# Initialize Cloud
websession = hass.helpers.aiohttp_client.async_get_clientsession()
client = CloudClient(hass, prefs, websession, alexa_conf, google_conf)
cloud = hass.data[DOMAIN] = Cloud(client, **kwargs)
async def _startup(event):
"""Startup event."""
await cloud.start()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _startup)
async def _shutdown(event):
"""Shutdown event."""
await cloud.stop()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
async def _service_handler(service):
"""Handle service for cloud."""
if service.service == SERVICE_REMOTE_CONNECT:
await cloud.remote.connect()
await prefs.async_update(remote_enabled=True)
elif service.service == SERVICE_REMOTE_DISCONNECT:
await cloud.remote.disconnect()
await prefs.async_update(remote_enabled=False)
hass.services.async_register(
DOMAIN, SERVICE_REMOTE_CONNECT, _service_handler)
hass.services.async_register(
DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler)
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
await auth_api.async_setup(hass, cloud)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, cloud.async_start)
await http_api.async_setup(hass)
hass.async_create_task(hass.helpers.discovery.async_load_platform(
'binary_sensor', DOMAIN, {}, config))
return True
class Cloud:
"""Store the configuration of the cloud connection."""
def __init__(self, hass, mode, alexa, google_actions,
cognito_client_id=None, user_pool_id=None, region=None,
relayer=None, google_actions_sync_url=None,
subscription_info_url=None, cloudhook_create_url=None):
"""Create an instance of Cloud."""
self.hass = hass
self.mode = mode
self.alexa_config = alexa
self.google_actions_user_conf = google_actions
self._gactions_config = None
self.prefs = prefs.CloudPreferences(hass)
self.id_token = None
self.access_token = None
self.refresh_token = None
self.iot = iot.CloudIoT(self)
self.cloudhooks = cloudhooks.Cloudhooks(self)
if mode == MODE_DEV:
self.cognito_client_id = cognito_client_id
self.user_pool_id = user_pool_id
self.region = region
self.relayer = relayer
self.google_actions_sync_url = google_actions_sync_url
self.subscription_info_url = subscription_info_url
self.cloudhook_create_url = cloudhook_create_url
else:
info = SERVERS[mode]
self.cognito_client_id = info['cognito_client_id']
self.user_pool_id = info['user_pool_id']
self.region = info['region']
self.relayer = info['relayer']
self.google_actions_sync_url = info['google_actions_sync_url']
self.subscription_info_url = info['subscription_info_url']
self.cloudhook_create_url = info['cloudhook_create_url']
@property
def is_logged_in(self):
"""Get if cloud is logged in."""
return self.id_token is not None
@property
def subscription_expired(self):
"""Return a boolean if the subscription has expired."""
return dt_util.utcnow() > self.expiration_date + timedelta(days=7)
@property
def expiration_date(self):
"""Return the subscription expiration as a UTC datetime object."""
return datetime.combine(
dt_util.parse_date(self.claims['custom:sub-exp']),
datetime.min.time()).replace(tzinfo=dt_util.UTC)
@property
def claims(self):
"""Return the claims from the id token."""
return self._decode_claims(self.id_token)
@property
def user_info_path(self):
"""Get path to the stored auth."""
return self.path('{}_auth.json'.format(self.mode))
@property
def gactions_config(self):
"""Return the Google Assistant config."""
if self._gactions_config is None:
conf = self.google_actions_user_conf
def should_expose(entity):
"""If an entity should be exposed."""
if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False
return conf['filter'](entity.entity_id)
self._gactions_config = ga_h.Config(
should_expose=should_expose,
allow_unlock=self.prefs.google_allow_unlock,
agent_user_id=self.claims['cognito:username'],
entity_config=conf.get(CONF_ENTITY_CONFIG),
)
return self._gactions_config
def path(self, *parts):
"""Get config path inside cloud dir.
Async friendly.
"""
return self.hass.config.path(CONFIG_DIR, *parts)
async def fetch_subscription_info(self):
"""Fetch subscription info."""
await self.hass.async_add_executor_job(auth_api.check_token, self)
websession = self.hass.helpers.aiohttp_client.async_get_clientsession()
return await websession.get(
self.subscription_info_url, headers={
'authorization': self.id_token
})
async def logout(self):
"""Close connection and remove all credentials."""
await self.iot.disconnect()
self.id_token = None
self.access_token = None
self.refresh_token = None
self._gactions_config = None
await self.hass.async_add_job(
lambda: os.remove(self.user_info_path))
def write_user_info(self):
"""Write user info to a file."""
with open(self.user_info_path, 'wt') as file:
file.write(json.dumps({
'id_token': self.id_token,
'access_token': self.access_token,
'refresh_token': self.refresh_token,
}, indent=4))
async def async_start(self, _):
"""Start the cloud component."""
def load_config():
"""Load config."""
# Ensure config dir exists
path = self.hass.config.path(CONFIG_DIR)
if not os.path.isdir(path):
os.mkdir(path)
user_info = self.user_info_path
if not os.path.isfile(user_info):
return None
with open(user_info, 'rt') as file:
return json.loads(file.read())
info = await self.hass.async_add_job(load_config)
await self.prefs.async_initialize()
if info is None:
return
self.id_token = info['id_token']
self.access_token = info['access_token']
self.refresh_token = info['refresh_token']
self.hass.async_create_task(self.iot.connect())
def _decode_claims(self, token): # pylint: disable=no-self-use
"""Decode the claims in a token."""
from jose import jwt
return jwt.get_unverified_claims(token)

View File

@@ -1,232 +0,0 @@
"""Package to communicate with the authentication API."""
import asyncio
import logging
import random
_LOGGER = logging.getLogger(__name__)
class CloudError(Exception):
"""Base class for cloud related errors."""
class Unauthenticated(CloudError):
"""Raised when authentication failed."""
class UserNotFound(CloudError):
"""Raised when a user is not found."""
class UserNotConfirmed(CloudError):
"""Raised when a user has not confirmed email yet."""
class PasswordChangeRequired(CloudError):
"""Raised when a password change is required."""
# https://github.com/PyCQA/pylint/issues/1085
# pylint: disable=useless-super-delegation
def __init__(self, message='Password change required.'):
"""Initialize a password change required error."""
super().__init__(message)
class UnknownError(CloudError):
"""Raised when an unknown error occurs."""
AWS_EXCEPTIONS = {
'UserNotFoundException': UserNotFound,
'NotAuthorizedException': Unauthenticated,
'UserNotConfirmedException': UserNotConfirmed,
'PasswordResetRequiredException': PasswordChangeRequired,
}
async def async_setup(hass, cloud):
"""Configure the auth api."""
refresh_task = None
async def handle_token_refresh():
"""Handle Cloud access token refresh."""
sleep_time = 5
sleep_time = random.randint(2400, 3600)
while True:
try:
await asyncio.sleep(sleep_time)
await hass.async_add_executor_job(renew_access_token, cloud)
except CloudError as err:
_LOGGER.error("Can't refresh cloud token: %s", err)
except asyncio.CancelledError:
# Task is canceled, stop it.
break
sleep_time = random.randint(3100, 3600)
async def on_connect():
"""When the instance is connected."""
nonlocal refresh_task
refresh_task = hass.async_create_task(handle_token_refresh())
async def on_disconnect():
"""When the instance is disconnected."""
nonlocal refresh_task
refresh_task.cancel()
cloud.iot.register_on_connect(on_connect)
cloud.iot.register_on_disconnect(on_disconnect)
def _map_aws_exception(err):
"""Map AWS exception to our exceptions."""
ex = AWS_EXCEPTIONS.get(err.response['Error']['Code'], UnknownError)
return ex(err.response['Error']['Message'])
def register(cloud, email, password):
"""Register a new account."""
from botocore.exceptions import ClientError, EndpointConnectionError
cognito = _cognito(cloud)
# Workaround for bug in Warrant. PR with fix:
# https://github.com/capless/warrant/pull/82
cognito.add_base_attributes()
try:
cognito.register(email, password)
except ClientError as err:
raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def resend_email_confirm(cloud, email):
"""Resend email confirmation."""
from botocore.exceptions import ClientError, EndpointConnectionError
cognito = _cognito(cloud, username=email)
try:
cognito.client.resend_confirmation_code(
Username=email,
ClientId=cognito.client_id
)
except ClientError as err:
raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def forgot_password(cloud, email):
"""Initialize forgotten password flow."""
from botocore.exceptions import ClientError, EndpointConnectionError
cognito = _cognito(cloud, username=email)
try:
cognito.initiate_forgot_password()
except ClientError as err:
raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def login(cloud, email, password):
"""Log user in and fetch certificate."""
cognito = _authenticate(cloud, email, password)
cloud.id_token = cognito.id_token
cloud.access_token = cognito.access_token
cloud.refresh_token = cognito.refresh_token
cloud.write_user_info()
def check_token(cloud):
"""Check that the token is valid and verify if needed."""
from botocore.exceptions import ClientError, EndpointConnectionError
cognito = _cognito(
cloud,
access_token=cloud.access_token,
refresh_token=cloud.refresh_token)
try:
if cognito.check_token():
cloud.id_token = cognito.id_token
cloud.access_token = cognito.access_token
cloud.write_user_info()
except ClientError as err:
raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def renew_access_token(cloud):
"""Renew access token."""
from botocore.exceptions import ClientError, EndpointConnectionError
cognito = _cognito(
cloud,
access_token=cloud.access_token,
refresh_token=cloud.refresh_token)
try:
cognito.renew_access_token()
cloud.id_token = cognito.id_token
cloud.access_token = cognito.access_token
cloud.write_user_info()
except ClientError as err:
raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def _authenticate(cloud, email, password):
"""Log in and return an authenticated Cognito instance."""
from botocore.exceptions import ClientError, EndpointConnectionError
from warrant.exceptions import ForceChangePasswordException
assert not cloud.is_logged_in, 'Cannot login if already logged in.'
cognito = _cognito(cloud, username=email)
try:
cognito.authenticate(password=password)
return cognito
except ForceChangePasswordException:
raise PasswordChangeRequired()
except ClientError as err:
raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def _cognito(cloud, **kwargs):
"""Get the client credentials."""
import botocore
import boto3
from warrant import Cognito
cognito = Cognito(
user_pool_id=cloud.user_pool_id,
client_id=cloud.cognito_client_id,
user_pool_region=cloud.region,
**kwargs
)
cognito.client = boto3.client(
'cognito-idp',
region_name=cloud.region,
config=botocore.config.Config(
signature_version=botocore.UNSIGNED
)
)
return cognito

View File

@@ -0,0 +1,73 @@
"""Support for Home Assistant Cloud binary sensors."""
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN
DEPENDENCIES = ['cloud']
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Set up the cloud binary sensors."""
if discovery_info is None:
return
cloud = hass.data[DOMAIN]
async_add_entities([CloudRemoteBinary(cloud)])
class CloudRemoteBinary(BinarySensorDevice):
"""Representation of an Cloud Remote UI Connection binary sensor."""
def __init__(self, cloud):
"""Initialize the binary sensor."""
self.cloud = cloud
self._unsub_dispatcher = None
@property
def name(self) -> str:
"""Return the name of the binary sensor, if any."""
return "Remote UI"
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return "cloud-remote-ui-connectivity"
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return self.cloud.remote.is_connected
@property
def device_class(self) -> str:
"""Return the class of this device, from component DEVICE_CLASSES."""
return 'connectivity'
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.cloud.remote.certificate is not None
@property
def should_poll(self) -> bool:
"""Return True if entity has to be polled for state."""
return False
async def async_added_to_hass(self):
"""Register update dispatcher."""
@callback
def async_state_update(data):
"""Update callback."""
self.async_write_ha_state()
self._unsub_dispatcher = async_dispatcher_connect(
self.hass, DISPATCHER_REMOTE_UPDATE, async_state_update)
async def async_will_remove_from_hass(self):
"""Register update dispatcher."""
if self._unsub_dispatcher is not None:
self._unsub_dispatcher()
self._unsub_dispatcher = None

View File

@@ -0,0 +1,198 @@
"""Interface implementation for cloud client."""
import asyncio
from pathlib import Path
from typing import Any, Dict
import aiohttp
from hass_nabucasa.client import CloudClient as Interface
from homeassistant.core import callback
from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import (
helpers as ga_h, smart_home as ga)
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.util.aiohttp import MockRequest
from . import utils
from .const import (
CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE)
from .prefs import CloudPreferences
class CloudClient(Interface):
"""Interface class for Home Assistant Cloud."""
def __init__(self, hass: HomeAssistantType, prefs: CloudPreferences,
websession: aiohttp.ClientSession,
alexa_config: Dict[str, Any], google_config: Dict[str, Any]):
"""Initialize client interface to Cloud."""
self._hass = hass
self._prefs = prefs
self._websession = websession
self._alexa_user_config = alexa_config
self._google_user_config = google_config
self._alexa_config = None
self._google_config = None
@property
def base_path(self) -> Path:
"""Return path to base dir."""
return Path(self._hass.config.config_dir)
@property
def prefs(self) -> CloudPreferences:
"""Return Cloud preferences."""
return self._prefs
@property
def loop(self) -> asyncio.BaseEventLoop:
"""Return client loop."""
return self._hass.loop
@property
def websession(self) -> aiohttp.ClientSession:
"""Return client session for aiohttp."""
return self._websession
@property
def aiohttp_runner(self) -> aiohttp.web.AppRunner:
"""Return client webinterface aiohttp application."""
return self._hass.http.runner
@property
def cloudhooks(self) -> Dict[str, Dict[str, str]]:
"""Return list of cloudhooks."""
return self._prefs.cloudhooks
@property
def remote_autostart(self) -> bool:
"""Return true if we want start a remote connection."""
return self._prefs.remote_enabled
@property
def alexa_config(self) -> alexa_sh.Config:
"""Return Alexa config."""
if not self._alexa_config:
alexa_conf = self._alexa_user_config
self._alexa_config = alexa_sh.Config(
endpoint=None,
async_get_access_token=None,
should_expose=alexa_conf[CONF_FILTER],
entity_config=alexa_conf.get(CONF_ENTITY_CONFIG),
)
return self._alexa_config
@property
def google_config(self) -> ga_h.Config:
"""Return Google config."""
if not self._google_config:
google_conf = self._google_user_config
def should_expose(entity):
"""If an entity should be exposed."""
if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False
return google_conf['filter'](entity.entity_id)
self._google_config = ga_h.Config(
should_expose=should_expose,
allow_unlock=self._prefs.google_allow_unlock,
entity_config=google_conf.get(CONF_ENTITY_CONFIG),
)
return self._google_config
@property
def google_user_config(self) -> Dict[str, Any]:
"""Return google action user config."""
return self._google_user_config
async def cleanups(self) -> None:
"""Cleanup some stuff after logout."""
self._alexa_config = None
self._google_config = None
@callback
def user_message(self, identifier: str, title: str, message: str) -> None:
"""Create a message for user to UI."""
self._hass.components.persistent_notification.async_create(
message, title, identifier
)
@callback
def dispatcher_message(self, identifier: str, data: Any = None) -> None:
"""Match cloud notification to dispatcher."""
if identifier.startswith("remote_"):
async_dispatcher_send(self._hass, DISPATCHER_REMOTE_UPDATE, data)
async def async_alexa_message(
self, payload: Dict[Any, Any]) -> Dict[Any, Any]:
"""Process cloud alexa message to client."""
return await alexa_sh.async_handle_message(
self._hass, self.alexa_config, payload,
enabled=self._prefs.alexa_enabled
)
async def async_google_message(
self, payload: Dict[Any, Any]) -> Dict[Any, Any]:
"""Process cloud google message to client."""
if not self._prefs.google_enabled:
return ga.turned_off_response(payload)
answer = await ga.async_handle_message(
self._hass, self.google_config, self.prefs.cloud_user, payload
)
# Fix AgentUserId
cloud = self._hass.data[DOMAIN]
answer['payload']['agentUserId'] = cloud.claims['cognito:username']
return answer
async def async_webhook_message(
self, payload: Dict[Any, Any]) -> Dict[Any, Any]:
"""Process cloud webhook message to client."""
cloudhook_id = payload['cloudhook_id']
found = None
for cloudhook in self._prefs.cloudhooks.values():
if cloudhook['cloudhook_id'] == cloudhook_id:
found = cloudhook
break
if found is None:
return {
'status': 200
}
request = MockRequest(
content=payload['body'].encode('utf-8'),
headers=payload['headers'],
method=payload['method'],
query_string=payload['query'],
)
response = await self._hass.components.webhook.async_handle_webhook(
found['webhook_id'], request)
response_dict = utils.aiohttp_serialize_response(response)
body = response_dict.get('body')
return {
'body': body,
'status': response_dict['status'],
'headers': {
'Content-Type': response.content_type
}
}
async def async_cloudhooks_update(
self, data: Dict[str, Dict[str, str]]) -> None:
"""Update local list of cloudhooks."""
await self._prefs.async_update(cloudhooks=data)

View File

@@ -1,42 +0,0 @@
"""Cloud APIs."""
from functools import wraps
import logging
from . import auth_api
_LOGGER = logging.getLogger(__name__)
def _check_token(func):
"""Decorate a function to verify valid token."""
@wraps(func)
async def check_token(cloud, *args):
"""Validate token, then call func."""
await cloud.hass.async_add_executor_job(auth_api.check_token, cloud)
return await func(cloud, *args)
return check_token
def _log_response(func):
"""Decorate a function to log bad responses."""
@wraps(func)
async def log_response(*args):
"""Log response if it's bad."""
resp = await func(*args)
meth = _LOGGER.debug if resp.status < 400 else _LOGGER.warning
meth('Fetched %s (%s)', resp.url, resp.status)
return resp
return log_response
@_check_token
@_log_response
async def async_create_cloudhook(cloud):
"""Create a cloudhook."""
websession = cloud.hass.helpers.aiohttp_client.async_get_clientsession()
return await websession.post(
cloud.cloudhook_create_url, headers={
'authorization': cloud.id_token
})

View File

@@ -1,66 +0,0 @@
"""Manage cloud cloudhooks."""
import async_timeout
from . import cloud_api
class Cloudhooks:
"""Class to help manage cloudhooks."""
def __init__(self, cloud):
"""Initialize cloudhooks."""
self.cloud = cloud
self.cloud.iot.register_on_connect(self.async_publish_cloudhooks)
async def async_publish_cloudhooks(self):
"""Inform the Relayer of the cloudhooks that we support."""
cloudhooks = self.cloud.prefs.cloudhooks
await self.cloud.iot.async_send_message('webhook-register', {
'cloudhook_ids': [info['cloudhook_id'] for info
in cloudhooks.values()]
}, expect_answer=False)
async def async_create(self, webhook_id):
"""Create a cloud webhook."""
cloudhooks = self.cloud.prefs.cloudhooks
if webhook_id in cloudhooks:
raise ValueError('Hook is already enabled for the cloud.')
if not self.cloud.iot.connected:
raise ValueError("Cloud is not connected")
# Create cloud hook
with async_timeout.timeout(10):
resp = await cloud_api.async_create_cloudhook(self.cloud)
data = await resp.json()
cloudhook_id = data['cloudhook_id']
cloudhook_url = data['url']
# Store hook
cloudhooks = dict(cloudhooks)
hook = cloudhooks[webhook_id] = {
'webhook_id': webhook_id,
'cloudhook_id': cloudhook_id,
'cloudhook_url': cloudhook_url
}
await self.cloud.prefs.async_update(cloudhooks=cloudhooks)
await self.async_publish_cloudhooks()
return hook
async def async_delete(self, webhook_id):
"""Delete a cloud webhook."""
cloudhooks = self.cloud.prefs.cloudhooks
if webhook_id not in cloudhooks:
raise ValueError('Hook is not enabled for the cloud.')
# Remove hook
cloudhooks = dict(cloudhooks)
cloudhooks.pop(webhook_id)
await self.cloud.prefs.async_update(cloudhooks=cloudhooks)
await self.async_publish_cloudhooks()

View File

@@ -1,34 +1,33 @@
"""Constants for the cloud component."""
DOMAIN = 'cloud'
CONFIG_DIR = '.cloud'
REQUEST_TIMEOUT = 10
PREF_ENABLE_ALEXA = 'alexa_enabled'
PREF_ENABLE_GOOGLE = 'google_enabled'
PREF_ENABLE_REMOTE = 'remote_enabled'
PREF_GOOGLE_ALLOW_UNLOCK = 'google_allow_unlock'
PREF_CLOUDHOOKS = 'cloudhooks'
PREF_CLOUD_USER = 'cloud_user'
SERVERS = {
'production': {
'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u',
'user_pool_id': 'us-east-1_87ll5WOP8',
'region': 'us-east-1',
'relayer': 'wss://cloud.hass.io:8000/websocket',
'google_actions_sync_url': ('https://24ab3v80xd.execute-api.us-east-1.'
'amazonaws.com/prod/smart_home_sync'),
'subscription_info_url': ('https://stripe-api.nabucasa.com/payments/'
'subscription_info'),
'cloudhook_create_url': 'https://webhooks-api.nabucasa.com/generate'
}
}
CONF_ALEXA = 'alexa'
CONF_ALIASES = 'aliases'
CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
CONF_ENTITY_CONFIG = 'entity_config'
CONF_FILTER = 'filter'
CONF_GOOGLE_ACTIONS = 'google_actions'
CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id'
CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url'
CONF_SUBSCRIPTION_INFO_URL = 'subscription_info_url'
CONF_CLOUDHOOK_CREATE_URL = 'cloudhook_create_url'
CONF_REMOTE_API_URL = 'remote_api_url'
CONF_ACME_DIRECTORY_SERVER = 'acme_directory_server'
MESSAGE_EXPIRATION = """
It looks like your Home Assistant Cloud subscription has expired. Please check
your [account page](/config/cloud/account) to continue using the service.
"""
MODE_DEV = "development"
MODE_PROD = "production"
MESSAGE_AUTH_FAIL = """
You have been logged out of Home Assistant Cloud because we have been unable
to verify your credentials. Please [log in](/config/cloud) again to continue
using the service.
"""
DISPATCHER_REMOTE_UPDATE = 'cloud_remote_update'
class InvalidTrustedNetworks(Exception):
"""Raised when invalid trusted networks config."""

View File

@@ -3,6 +3,7 @@ import asyncio
from functools import wraps
import logging
import attr
import aiohttp
import async_timeout
import voluptuous as vol
@@ -15,11 +16,9 @@ from homeassistant.components import websocket_api
from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import smart_home as google_sh
from . import auth_api
from .const import (
DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
PREF_GOOGLE_ALLOW_UNLOCK)
from .iot import STATE_DISCONNECTED, STATE_CONNECTED
PREF_GOOGLE_ALLOW_UNLOCK, InvalidTrustedNetworks)
_LOGGER = logging.getLogger(__name__)
@@ -59,6 +58,13 @@ SCHEMA_WS_HOOK_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
})
_CLOUD_ERRORS = {
InvalidTrustedNetworks:
(500, 'Remote UI not compatible with 127.0.0.1/::1'
' as a trusted network.')
}
async def async_setup(hass):
"""Initialize the HTTP API."""
hass.components.websocket_api.async_register_command(
@@ -81,6 +87,10 @@ async def async_setup(hass):
WS_TYPE_HOOK_DELETE, websocket_hook_delete,
SCHEMA_WS_HOOK_DELETE
)
hass.components.websocket_api.async_register_command(
websocket_remote_connect)
hass.components.websocket_api.async_register_command(
websocket_remote_disconnect)
hass.http.register_view(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView)
@@ -88,14 +98,22 @@ async def async_setup(hass):
hass.http.register_view(CloudResendConfirmView)
hass.http.register_view(CloudForgotPasswordView)
from hass_nabucasa import auth
_CLOUD_ERRORS = {
auth_api.UserNotFound: (400, "User does not exist."),
auth_api.UserNotConfirmed: (400, 'Email not confirmed.'),
auth_api.Unauthenticated: (401, 'Authentication failed.'),
auth_api.PasswordChangeRequired: (400, 'Password change required.'),
asyncio.TimeoutError: (502, 'Unable to reach the Home Assistant cloud.')
}
_CLOUD_ERRORS.update({
auth.UserNotFound:
(400, "User does not exist."),
auth.UserNotConfirmed:
(400, 'Email not confirmed.'),
auth.Unauthenticated:
(401, 'Authentication failed.'),
auth.PasswordChangeRequired:
(400, 'Password change required.'),
asyncio.TimeoutError:
(502, 'Unable to reach the Home Assistant cloud.'),
aiohttp.ClientError:
(500, 'Error making internal request'),
})
def _handle_cloud_errors(handler):
@@ -108,12 +126,7 @@ def _handle_cloud_errors(handler):
return result
except Exception as err: # pylint: disable=broad-except
err_info = _CLOUD_ERRORS.get(err.__class__)
if err_info is None:
_LOGGER.exception(
"Unexpected error processing request for %s", request.path)
err_info = (502, 'Unexpected error: {}'.format(err))
status, msg = err_info
status, msg = _process_cloud_exception(err, request.path)
return view.json_message(
msg, status_code=status,
message_code=err.__class__.__name__.lower())
@@ -121,6 +134,31 @@ def _handle_cloud_errors(handler):
return error_handler
def _ws_handle_cloud_errors(handler):
"""Websocket decorator to handle auth errors."""
@wraps(handler)
async def error_handler(hass, connection, msg):
"""Handle exceptions that raise from the wrapped handler."""
try:
return await handler(hass, connection, msg)
except Exception as err: # pylint: disable=broad-except
err_status, err_msg = _process_cloud_exception(err, msg['type'])
connection.send_error(msg['id'], err_status, err_msg)
return error_handler
def _process_cloud_exception(exc, where):
"""Process a cloud exception."""
err_info = _CLOUD_ERRORS.get(exc.__class__)
if err_info is None:
_LOGGER.exception(
"Unexpected error processing request for %s", where)
err_info = (502, 'Unexpected error: {}'.format(exc))
return err_info
class GoogleActionsSyncView(HomeAssistantView):
"""Trigger a Google Actions Smart Home Sync."""
@@ -135,7 +173,7 @@ class GoogleActionsSyncView(HomeAssistantView):
websession = hass.helpers.aiohttp_client.async_get_clientsession()
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
await hass.async_add_job(auth_api.check_token, cloud)
await hass.async_add_job(cloud.auth.check_token)
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
req = await websession.post(
@@ -163,7 +201,7 @@ class CloudLoginView(HomeAssistantView):
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
await hass.async_add_job(auth_api.login, cloud, data['email'],
await hass.async_add_job(cloud.auth.login, data['email'],
data['password'])
hass.async_add_job(cloud.iot.connect)
@@ -206,7 +244,7 @@ class CloudRegisterView(HomeAssistantView):
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
await hass.async_add_job(
auth_api.register, cloud, data['email'], data['password'])
cloud.auth.register, data['email'], data['password'])
return self.json_message('ok')
@@ -228,7 +266,7 @@ class CloudResendConfirmView(HomeAssistantView):
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
await hass.async_add_job(
auth_api.resend_email_confirm, cloud, data['email'])
cloud.auth.resend_email_confirm, data['email'])
return self.json_message('ok')
@@ -250,7 +288,7 @@ class CloudForgotPasswordView(HomeAssistantView):
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
await hass.async_add_job(
auth_api.forgot_password, cloud, data['email'])
cloud.auth.forgot_password, data['email'])
return self.json_message('ok')
@@ -283,30 +321,11 @@ def _require_cloud_login(handler):
return with_cloud_auth
def _handle_aiohttp_errors(handler):
"""Websocket decorator that handlers aiohttp errors.
Can only wrap async handlers.
"""
@wraps(handler)
async def with_error_handling(hass, connection, msg):
"""Handle aiohttp errors."""
try:
await handler(hass, connection, msg)
except asyncio.TimeoutError:
connection.send_message(websocket_api.error_message(
msg['id'], 'timeout', 'Command timed out.'))
except aiohttp.ClientError:
connection.send_message(websocket_api.error_message(
msg['id'], 'unknown', 'Error making request.'))
return with_error_handling
@_require_cloud_login
@websocket_api.async_response
async def websocket_subscription(hass, connection, msg):
"""Handle request for account info."""
from hass_nabucasa.const import STATE_DISCONNECTED
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
@@ -320,11 +339,10 @@ async def websocket_subscription(hass, connection, msg):
# Check if a user is subscribed but local info is outdated
# In that case, let's refresh and reconnect
if data.get('provider') and cloud.iot.state != STATE_CONNECTED:
if data.get('provider') and not cloud.is_connected:
_LOGGER.debug(
"Found disconnected account with valid subscriotion, connecting")
await hass.async_add_executor_job(
auth_api.renew_access_token, cloud)
await hass.async_add_executor_job(cloud.auth.renew_access_token)
# Cancel reconnect in progress
if cloud.iot.state != STATE_DISCONNECTED:
@@ -344,23 +362,24 @@ async def websocket_update_prefs(hass, connection, msg):
changes = dict(msg)
changes.pop('id')
changes.pop('type')
await cloud.prefs.async_update(**changes)
await cloud.client.prefs.async_update(**changes)
connection.send_message(websocket_api.result_message(msg['id']))
@_require_cloud_login
@websocket_api.async_response
@_handle_aiohttp_errors
@_ws_handle_cloud_errors
async def websocket_hook_create(hass, connection, msg):
"""Handle request for account info."""
cloud = hass.data[DOMAIN]
hook = await cloud.cloudhooks.async_create(msg['webhook_id'])
hook = await cloud.cloudhooks.async_create(msg['webhook_id'], False)
connection.send_message(websocket_api.result_message(msg['id'], hook))
@_require_cloud_login
@websocket_api.async_response
@_ws_handle_cloud_errors
async def websocket_hook_delete(hass, connection, msg):
"""Handle request for account info."""
cloud = hass.data[DOMAIN]
@@ -370,6 +389,8 @@ async def websocket_hook_delete(hass, connection, msg):
def _account_data(cloud):
"""Generate the auth data JSON response."""
from hass_nabucasa.const import STATE_DISCONNECTED
if not cloud.is_logged_in:
return {
'logged_in': False,
@@ -377,14 +398,53 @@ def _account_data(cloud):
}
claims = cloud.claims
client = cloud.client
remote = cloud.remote
# Load remote certificate
if remote.certificate:
certificate = attr.asdict(remote.certificate)
else:
certificate = None
return {
'logged_in': True,
'email': claims['email'],
'cloud': cloud.iot.state,
'prefs': cloud.prefs.as_dict(),
'google_entities': cloud.google_actions_user_conf['filter'].config,
'prefs': client.prefs.as_dict(),
'google_entities': client.google_user_config['filter'].config,
'google_domains': list(google_sh.DOMAIN_TO_GOOGLE_TYPES),
'alexa_entities': cloud.alexa_config.should_expose.config,
'alexa_entities': client.alexa_config.should_expose.config,
'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS),
'remote_domain': remote.instance_domain,
'remote_connected': remote.is_connected,
'remote_certificate': certificate,
}
@_require_cloud_login
@websocket_api.async_response
@_ws_handle_cloud_errors
@websocket_api.websocket_command({
'type': 'cloud/remote/connect'
})
async def websocket_remote_connect(hass, connection, msg):
"""Handle request for connect remote."""
cloud = hass.data[DOMAIN]
await cloud.client.prefs.async_update(remote_enabled=True)
await cloud.remote.connect()
connection.send_result(msg['id'], _account_data(cloud))
@_require_cloud_login
@websocket_api.async_response
@_ws_handle_cloud_errors
@websocket_api.websocket_command({
'type': 'cloud/remote/disconnect'
})
async def websocket_remote_disconnect(hass, connection, msg):
"""Handle request for disconnect remote."""
cloud = hass.data[DOMAIN]
await cloud.client.prefs.async_update(remote_enabled=False)
await cloud.remote.disconnect()
connection.send_result(msg['id'], _account_data(cloud))

View File

@@ -1,391 +0,0 @@
"""Module to handle messages from Home Assistant cloud."""
import asyncio
import logging
import pprint
import random
import uuid
from aiohttp import hdrs, client_exceptions, WSMsgType
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.components.alexa import smart_home as alexa
from homeassistant.components.google_assistant import smart_home as ga
from homeassistant.core import callback
from homeassistant.util.decorator import Registry
from homeassistant.util.aiohttp import MockRequest
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import auth_api
from . import utils
from .const import MESSAGE_EXPIRATION, MESSAGE_AUTH_FAIL
HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__)
STATE_CONNECTING = 'connecting'
STATE_CONNECTED = 'connected'
STATE_DISCONNECTED = 'disconnected'
class UnknownHandler(Exception):
"""Exception raised when trying to handle unknown handler."""
class NotConnected(Exception):
"""Exception raised when trying to handle unknown handler."""
class ErrorMessage(Exception):
"""Exception raised when there was error handling message in the cloud."""
def __init__(self, error):
"""Initialize Error Message."""
super().__init__(self, "Error in Cloud")
self.error = error
class CloudIoT:
"""Class to manage the IoT connection."""
def __init__(self, cloud):
"""Initialize the CloudIoT class."""
self.cloud = cloud
# The WebSocket client
self.client = None
# Scheduled sleep task till next connection retry
self.retry_task = None
# Boolean to indicate if we wanted the connection to close
self.close_requested = False
# The current number of attempts to connect, impacts wait time
self.tries = 0
# Current state of the connection
self.state = STATE_DISCONNECTED
# Local code waiting for a response
self._response_handler = {}
self._on_connect = []
self._on_disconnect = []
@callback
def register_on_connect(self, on_connect_cb):
"""Register an async on_connect callback."""
self._on_connect.append(on_connect_cb)
@callback
def register_on_disconnect(self, on_disconnect_cb):
"""Register an async on_disconnect callback."""
self._on_disconnect.append(on_disconnect_cb)
@property
def connected(self):
"""Return if we're currently connected."""
return self.state == STATE_CONNECTED
@asyncio.coroutine
def connect(self):
"""Connect to the IoT broker."""
if self.state != STATE_DISCONNECTED:
raise RuntimeError('Connect called while not disconnected')
hass = self.cloud.hass
self.close_requested = False
self.state = STATE_CONNECTING
self.tries = 0
@asyncio.coroutine
def _handle_hass_stop(event):
"""Handle Home Assistant shutting down."""
nonlocal remove_hass_stop_listener
remove_hass_stop_listener = None
yield from self.disconnect()
remove_hass_stop_listener = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, _handle_hass_stop)
while True:
try:
yield from self._handle_connection()
except Exception: # pylint: disable=broad-except
# Safety net. This should never hit.
# Still adding it here to make sure we can always reconnect
_LOGGER.exception("Unexpected error")
if self.state == STATE_CONNECTED and self._on_disconnect:
try:
yield from asyncio.wait([
cb() for cb in self._on_disconnect
])
except Exception: # pylint: disable=broad-except
# Safety net. This should never hit.
# Still adding it here to make sure we don't break the flow
_LOGGER.exception(
"Unexpected error in on_disconnect callbacks")
if self.close_requested:
break
self.state = STATE_CONNECTING
self.tries += 1
try:
# Sleep 2^tries + 0…tries*3 seconds between retries
self.retry_task = hass.async_create_task(
asyncio.sleep(2**min(9, self.tries) +
random.randint(0, self.tries * 3),
loop=hass.loop))
yield from self.retry_task
self.retry_task = None
except asyncio.CancelledError:
# Happens if disconnect called
break
self.state = STATE_DISCONNECTED
if remove_hass_stop_listener is not None:
remove_hass_stop_listener()
async def async_send_message(self, handler, payload,
expect_answer=True):
"""Send a message."""
if self.state != STATE_CONNECTED:
raise NotConnected
msgid = uuid.uuid4().hex
if expect_answer:
fut = self._response_handler[msgid] = asyncio.Future()
message = {
'msgid': msgid,
'handler': handler,
'payload': payload,
}
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug("Publishing message:\n%s\n",
pprint.pformat(message))
await self.client.send_json(message)
if expect_answer:
return await fut
@asyncio.coroutine
def _handle_connection(self):
"""Connect to the IoT broker."""
hass = self.cloud.hass
try:
yield from hass.async_add_job(auth_api.check_token, self.cloud)
except auth_api.Unauthenticated as err:
_LOGGER.error('Unable to refresh token: %s', err)
hass.components.persistent_notification.async_create(
MESSAGE_AUTH_FAIL, 'Home Assistant Cloud',
'cloud_subscription_expired')
# Don't await it because it will cancel this task
hass.async_create_task(self.cloud.logout())
return
except auth_api.CloudError as err:
_LOGGER.warning("Unable to refresh token: %s", err)
return
if self.cloud.subscription_expired:
hass.components.persistent_notification.async_create(
MESSAGE_EXPIRATION, 'Home Assistant Cloud',
'cloud_subscription_expired')
self.close_requested = True
return
session = async_get_clientsession(self.cloud.hass)
client = None
disconnect_warn = None
try:
self.client = client = yield from session.ws_connect(
self.cloud.relayer, heartbeat=55, headers={
hdrs.AUTHORIZATION:
'Bearer {}'.format(self.cloud.id_token)
})
self.tries = 0
_LOGGER.info("Connected")
self.state = STATE_CONNECTED
if self._on_connect:
try:
yield from asyncio.wait([cb() for cb in self._on_connect])
except Exception: # pylint: disable=broad-except
# Safety net. This should never hit.
# Still adding it here to make sure we don't break the flow
_LOGGER.exception(
"Unexpected error in on_connect callbacks")
while not client.closed:
msg = yield from client.receive()
if msg.type in (WSMsgType.CLOSED, WSMsgType.CLOSING):
break
elif msg.type == WSMsgType.ERROR:
disconnect_warn = 'Connection error'
break
elif msg.type != WSMsgType.TEXT:
disconnect_warn = 'Received non-Text message: {}'.format(
msg.type)
break
try:
msg = msg.json()
except ValueError:
disconnect_warn = 'Received invalid JSON.'
break
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug("Received message:\n%s\n",
pprint.pformat(msg))
response_handler = self._response_handler.pop(msg['msgid'],
None)
if response_handler is not None:
if 'payload' in msg:
response_handler.set_result(msg["payload"])
else:
response_handler.set_exception(
ErrorMessage(msg['error']))
continue
response = {
'msgid': msg['msgid'],
}
try:
result = yield from async_handle_message(
hass, self.cloud, msg['handler'], msg['payload'])
# No response from handler
if result is None:
continue
response['payload'] = result
except UnknownHandler:
response['error'] = 'unknown-handler'
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error handling message")
response['error'] = 'exception'
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug("Publishing message:\n%s\n",
pprint.pformat(response))
yield from client.send_json(response)
except client_exceptions.WSServerHandshakeError as err:
if err.status == 401:
disconnect_warn = 'Invalid auth.'
self.close_requested = True
# Should we notify user?
else:
_LOGGER.warning("Unable to connect: %s", err)
except client_exceptions.ClientError as err:
_LOGGER.warning("Unable to connect: %s", err)
finally:
if disconnect_warn is None:
_LOGGER.info("Connection closed")
else:
_LOGGER.warning("Connection closed: %s", disconnect_warn)
@asyncio.coroutine
def disconnect(self):
"""Disconnect the client."""
self.close_requested = True
if self.client is not None:
yield from self.client.close()
elif self.retry_task is not None:
self.retry_task.cancel()
@asyncio.coroutine
def async_handle_message(hass, cloud, handler_name, payload):
"""Handle incoming IoT message."""
handler = HANDLERS.get(handler_name)
if handler is None:
raise UnknownHandler()
return (yield from handler(hass, cloud, payload))
@HANDLERS.register('alexa')
@asyncio.coroutine
def async_handle_alexa(hass, cloud, payload):
"""Handle an incoming IoT message for Alexa."""
result = yield from alexa.async_handle_message(
hass, cloud.alexa_config, payload,
enabled=cloud.prefs.alexa_enabled)
return result
@HANDLERS.register('google_actions')
@asyncio.coroutine
def async_handle_google_actions(hass, cloud, payload):
"""Handle an incoming IoT message for Google Actions."""
if not cloud.prefs.google_enabled:
return ga.turned_off_response(payload)
result = yield from ga.async_handle_message(
hass, cloud.gactions_config, payload)
return result
@HANDLERS.register('cloud')
async def async_handle_cloud(hass, cloud, payload):
"""Handle an incoming IoT message for cloud component."""
action = payload['action']
if action == 'logout':
# Log out of Home Assistant Cloud
await cloud.logout()
_LOGGER.error("You have been logged out from Home Assistant cloud: %s",
payload['reason'])
else:
_LOGGER.warning("Received unknown cloud action: %s", action)
@HANDLERS.register('webhook')
async def async_handle_webhook(hass, cloud, payload):
"""Handle an incoming IoT message for cloud webhooks."""
cloudhook_id = payload['cloudhook_id']
found = None
for cloudhook in cloud.prefs.cloudhooks.values():
if cloudhook['cloudhook_id'] == cloudhook_id:
found = cloudhook
break
if found is None:
return {
'status': 200
}
request = MockRequest(
content=payload['body'].encode('utf-8'),
headers=payload['headers'],
method=payload['method'],
query_string=payload['query'],
)
response = await hass.components.webhook.async_handle_webhook(
found['webhook_id'], request)
response_dict = utils.aiohttp_serialize_response(response)
body = response_dict.get('body')
return {
'body': body,
'status': response_dict['status'],
'headers': {
'Content-Type': response.content_type
}
}

View File

@@ -1,7 +1,10 @@
"""Preference management for cloud."""
from ipaddress import ip_address
from .const import (
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
PREF_GOOGLE_ALLOW_UNLOCK, PREF_CLOUDHOOKS)
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE,
PREF_GOOGLE_ALLOW_UNLOCK, PREF_CLOUDHOOKS, PREF_CLOUD_USER,
InvalidTrustedNetworks)
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
@@ -13,6 +16,7 @@ class CloudPreferences:
def __init__(self, hass):
"""Initialize cloud prefs."""
self._hass = hass
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
self._prefs = None
@@ -24,31 +28,52 @@ class CloudPreferences:
prefs = {
PREF_ENABLE_ALEXA: True,
PREF_ENABLE_GOOGLE: True,
PREF_ENABLE_REMOTE: False,
PREF_GOOGLE_ALLOW_UNLOCK: False,
PREF_CLOUDHOOKS: {}
PREF_CLOUDHOOKS: {},
PREF_CLOUD_USER: None,
}
self._prefs = prefs
async def async_update(self, *, google_enabled=_UNDEF,
alexa_enabled=_UNDEF, google_allow_unlock=_UNDEF,
cloudhooks=_UNDEF):
alexa_enabled=_UNDEF, remote_enabled=_UNDEF,
google_allow_unlock=_UNDEF, cloudhooks=_UNDEF,
cloud_user=_UNDEF):
"""Update user preferences."""
for key, value in (
(PREF_ENABLE_GOOGLE, google_enabled),
(PREF_ENABLE_ALEXA, alexa_enabled),
(PREF_ENABLE_REMOTE, remote_enabled),
(PREF_GOOGLE_ALLOW_UNLOCK, google_allow_unlock),
(PREF_CLOUDHOOKS, cloudhooks),
(PREF_CLOUD_USER, cloud_user),
):
if value is not _UNDEF:
self._prefs[key] = value
if remote_enabled is True and self._has_local_trusted_network:
raise InvalidTrustedNetworks
await self._store.async_save(self._prefs)
def as_dict(self):
"""Return dictionary version."""
return self._prefs
@property
def remote_enabled(self):
"""Return if remote is enabled on start."""
enabled = self._prefs.get(PREF_ENABLE_REMOTE, False)
if not enabled:
return False
if self._has_local_trusted_network:
return False
return True
@property
def alexa_enabled(self):
"""Return if Alexa is enabled."""
@@ -68,3 +93,24 @@ class CloudPreferences:
def cloudhooks(self):
"""Return the published cloud webhooks."""
return self._prefs.get(PREF_CLOUDHOOKS, {})
@property
def cloud_user(self) -> str:
"""Return ID from Home Assistant Cloud system user."""
return self._prefs.get(PREF_CLOUD_USER)
@property
def _has_local_trusted_network(self) -> bool:
"""Return if we allow localhost to bypass auth."""
local4 = ip_address('127.0.0.1')
local6 = ip_address('::1')
for prv in self._hass.auth.auth_providers:
if prv.type != 'trusted_networks':
continue
for network in prv.trusted_networks:
if local4 in network or local6 in network:
return True
return False

View File

@@ -0,0 +1,7 @@
# Describes the format for available light services
remote_connect:
description: Make instance UI available outside over NabuCasa cloud.
remote_disconnect:
description: Disconnect UI from NabuCasa cloud.

View File

@@ -1,13 +1,13 @@
"""Component to configure Home Assistant via an API."""
import asyncio
import importlib
import os
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import EVENT_COMPONENT_LOADED, CONF_ID
from homeassistant.setup import (
async_prepare_setup_platform, ATTR_COMPONENT)
from homeassistant.setup import ATTR_COMPONENT
from homeassistant.components.http import HomeAssistantView
from homeassistant.util.yaml import load_yaml, dump
@@ -24,7 +24,6 @@ SECTIONS = (
'device_registry',
'entity_registry',
'group',
'hassbian',
'script',
)
ON_DEMAND = ('zwave',)
@@ -37,8 +36,7 @@ async def async_setup(hass, config):
async def setup_panel(panel_name):
"""Set up a panel."""
panel = await async_prepare_setup_platform(
hass, config, DOMAIN, panel_name)
panel = importlib.import_module('.{}'.format(panel_name), __name__)
if not panel:
return

View File

@@ -8,8 +8,6 @@ from homeassistant.core import callback
from homeassistant.helpers.area_registry import async_get_registry
DEPENDENCIES = ['websocket_api']
WS_TYPE_LIST = 'config/area_registry/list'
SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_LIST,

View File

@@ -36,6 +36,7 @@ async def async_setup(hass):
WS_TYPE_CREATE, websocket_create,
SCHEMA_WS_CREATE
)
hass.components.websocket_api.async_register_command(websocket_update)
return True
@@ -84,6 +85,40 @@ async def websocket_create(hass, connection, msg):
}))
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command({
vol.Required('type'): 'config/auth/update',
vol.Required('user_id'): str,
vol.Optional('name'): str,
vol.Optional('group_ids'): [str]
})
async def websocket_update(hass, connection, msg):
"""Update a user."""
user = await hass.auth.async_get_user(msg.pop('user_id'))
if not user:
connection.send_message(websocket_api.error_message(
msg['id'], websocket_api.const.ERR_NOT_FOUND, 'User not found'))
return
if user.system_generated:
connection.send_message(websocket_api.error_message(
msg['id'], 'cannot_modify_system_generated',
'Unable to update system generated users.'))
return
msg.pop('type')
msg_id = msg.pop('id')
await hass.auth.async_update_user(user, **msg)
connection.send_message(
websocket_api.result_message(msg_id, {
'user': _user_info(user),
}))
def _user_info(user):
"""Format a user."""
return {

View File

@@ -122,7 +122,6 @@ async def websocket_delete(hass, connection, msg):
websocket_api.result_message(msg['id']))
@websocket_api.require_admin
@websocket_api.async_response
async def websocket_change_password(hass, connection, msg):
"""Change user password."""

View File

@@ -118,6 +118,16 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView):
# pylint: disable=no-value-for-parameter
return await super().post(request)
def _prepare_result_json(self, result):
"""Convert result to JSON."""
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return super()._prepare_result_json(result)
data = result.copy()
data['result'] = data['result'].entry_id
data.pop('data')
return data
class ConfigManagerFlowResourceView(FlowManagerResourceView):
"""View to interact with the flow manager."""
@@ -143,6 +153,16 @@ class ConfigManagerFlowResourceView(FlowManagerResourceView):
# pylint: disable=no-value-for-parameter
return await super().post(request, flow_id)
def _prepare_result_json(self, result):
"""Convert result to JSON."""
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return super()._prepare_result_json(result)
data = result.copy()
data['result'] = data['result'].entry_id
data.pop('data')
return data
class ConfigManagerAvailableFlowView(HomeAssistantView):
"""View to query available flows."""
@@ -175,7 +195,7 @@ class OptionManagerFlowIndexView(FlowManagerIndexView):
return await super().post(request)
class OptionManagerFlowResourceView(ConfigManagerFlowResourceView):
class OptionManagerFlowResourceView(FlowManagerResourceView):
"""View to interact with the option flow manager."""
url = '/api/config/config_entries/options/flow/{flow_id}'

View File

@@ -7,8 +7,6 @@ from homeassistant.components.websocket_api.decorators import (
from homeassistant.core import callback
from homeassistant.helpers.device_registry import async_get_registry
DEPENDENCIES = ['websocket_api']
WS_TYPE_LIST = 'config/device_registry/list'
SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_LIST,

View File

@@ -9,8 +9,6 @@ from homeassistant.components.websocket_api.decorators import (
async_response, require_admin)
from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ['websocket_api']
WS_TYPE_LIST = 'config/entity_registry/list'
SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_LIST,

View File

@@ -1,86 +0,0 @@
"""Component to interact with Hassbian tools."""
import json
import os
from homeassistant.components.http import HomeAssistantView
_TEST_OUTPUT = """
{
"suites":{
"libcec":{
"state":"Uninstalled",
"description":"Installs the libcec package for controlling CEC devices from this Pi"
},
"mosquitto":{
"state":"failed",
"description":"Installs the Mosquitto package for setting up a local MQTT server"
},
"openzwave":{
"state":"Uninstalled",
"description":"Installs the Open Z-wave package for setting up your zwave network"
},
"samba":{
"state":"installing",
"description":"Installs the samba package for sharing the hassbian configuration files over the Pi's network."
}
}
}
""" # noqa
async def async_setup(hass):
"""Set up the Hassbian config."""
# Test if is Hassbian
test_mode = 'FORCE_HASSBIAN' in os.environ
is_hassbian = test_mode
if not is_hassbian:
return False
hass.http.register_view(HassbianSuitesView(test_mode))
hass.http.register_view(HassbianSuiteInstallView(test_mode))
return True
async def hassbian_status(hass, test_mode=False):
"""Query for the Hassbian status."""
# Fetch real output when not in test mode
if test_mode:
return json.loads(_TEST_OUTPUT)
raise Exception('Real mode not implemented yet.')
class HassbianSuitesView(HomeAssistantView):
"""Hassbian packages endpoint."""
url = '/api/config/hassbian/suites'
name = 'api:config:hassbian:suites'
def __init__(self, test_mode):
"""Initialize suites view."""
self._test_mode = test_mode
async def get(self, request):
"""Request suite status."""
inp = await hassbian_status(request.app['hass'], self._test_mode)
return self.json(inp['suites'])
class HassbianSuiteInstallView(HomeAssistantView):
"""Hassbian packages endpoint."""
url = '/api/config/hassbian/suites/{suite}/install'
name = 'api:config:hassbian:suite'
def __init__(self, test_mode):
"""Initialize suite view."""
self._test_mode = test_mode
async def post(self, request, suite):
"""Request suite status."""
# do real install if not in test mode
return self.json({"status": "ok"})

View File

@@ -0,0 +1 @@
"""Add support for ClearPass Policy Manager."""

View File

@@ -0,0 +1,85 @@
"""
Support for ClearPass Policy Manager.
Allows tracking devices with CPPM.
"""
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, DOMAIN
)
from homeassistant.const import (
CONF_HOST, CONF_API_KEY
)
REQUIREMENTS = ['clearpasspy==1.0.2']
SCAN_INTERVAL = timedelta(seconds=120)
CLIENT_ID = 'client_id'
GRANT_TYPE = 'client_credentials'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CLIENT_ID): cv.string,
vol.Required(CONF_API_KEY): cv.string,
})
_LOGGER = logging.getLogger(__name__)
def get_scanner(hass, config):
"""Initialize Scanner."""
from clearpasspy import ClearPass
data = {
'server': config[DOMAIN][CONF_HOST],
'grant_type': GRANT_TYPE,
'secret': config[DOMAIN][CONF_API_KEY],
'client': config[DOMAIN][CLIENT_ID]
}
cppm = ClearPass(data)
if cppm.access_token is None:
return None
_LOGGER.debug("Successfully received Access Token")
return CPPMDeviceScanner(cppm)
class CPPMDeviceScanner(DeviceScanner):
"""Initialize class."""
def __init__(self, cppm):
"""Initialize class."""
self._cppm = cppm
self.results = None
def scan_devices(self):
"""Initialize scanner."""
self.get_cppm_data()
return [device['mac'] for device in self.results]
def get_device_name(self, device):
"""Retrieve device name."""
name = next((
result['name'] for result in self.results
if result['mac'] == device), None)
return name
def get_cppm_data(self):
"""Retrieve data from Aruba Clearpass and return parsed result."""
endpoints = self._cppm.get_endpoints(100)['_embedded']['items']
devices = []
for item in endpoints:
if self._cppm.online_status(item['mac_address']):
device = {
'mac': item['mac_address'],
'name': item['mac_address']
}
devices.append(device)
else:
continue
_LOGGER.debug("Devices: %s", devices)
self.results = devices

View File

@@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
"device_fail": "Erreur inattendue lors de la cr\u00e9ation du p\u00e9riph\u00e9rique.",
"device_timeout": "D\u00e9lai de connexion au p\u00e9riph\u00e9rique expir\u00e9."
},
"step": {
"user": {
"data": {
"host": "H\u00f4te"
},
"description": "Entrez l'adresse IP de votre Daikin AC.",
"title": "Configurer Daikin AC"
}
},
"title": "Daikin AC"
}
}

View File

@@ -0,0 +1,12 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "\u0e42\u0e2e\u0e2a\u0e15\u0e4c"
}
}
},
"title": "Daikin AC"
}
}

View File

@@ -10,7 +10,7 @@
"data": {
"host": "\u4e3b\u673a"
},
"description": "\u8f93\u5165\u60a8\u7684 Daikin \u7a7a\u8c03\u7684IP\u5730\u5740\u3002",
"description": "\u8f93\u5165\u60a8\u7684 Daikin \u7a7a\u8c03\u7684 IP \u5730\u5740\u3002",
"title": "\u914d\u7f6e Daikin \u7a7a\u8c03"
}
},

View File

@@ -17,7 +17,7 @@
"title": "Definiere das deCONZ-Gateway"
},
"link": {
"description": "Entsperre dein deCONZ-Gateway, um dich bei Home Assistant zu registrieren. \n\n 1. Gehe zu den deCONZ-Systemeinstellungen \n 2. Dr\u00fccke die Taste \"Gateway entsperren\"",
"description": "Entsperre dein deCONZ-Gateway, um es bei Home Assistant zu registrieren. \n\n 1. Gehe in die deCONZ-Systemeinstellungen \n 2. Dr\u00fccke die Taste \"Gateway entsperren\"",
"title": "Mit deCONZ verbinden"
},
"options": {

View File

@@ -12,12 +12,12 @@
"init": {
"data": {
"host": "Host",
"port": "Poort (standaard: '80')"
"port": "Poort"
},
"title": "Definieer deCONZ gateway"
},
"link": {
"description": "Ontgrendel je deCONZ gateway om te registreren met Home Assistant.\n\n1. Ga naar deCONZ systeeminstellingen\n2. Druk op de knop \"Gateway ontgrendelen\"",
"description": "Ontgrendel je deCONZ gateway om te registreren met Home Assistant.\n\n1. Ga naar deCONZ systeeminstellingen (Instellingen -> Gateway -> Geavanceerd)\n2. Druk op de knop \"Gateway ontgrendelen\"",
"title": "Koppel met deCONZ"
},
"options": {

View File

@@ -6,16 +6,17 @@ DEPENDENCIES = (
'cloud',
'config',
'conversation',
'discovery',
'frontend',
'history',
'logbook',
'map',
'mobile_app',
'person',
'script',
'sun',
'system_health',
'updater',
'zeroconf',
)

View File

@@ -41,17 +41,17 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
for dev_id, topic in devices.items():
@callback
def async_message_received(topic, payload, qos, dev_id=dev_id):
def async_message_received(msg, dev_id=dev_id):
"""Handle received MQTT message."""
try:
data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(payload))
data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(msg.payload))
except vol.MultipleInvalid:
_LOGGER.error("Skipping update for following data "
"because of missing or malformatted data: %s",
payload)
msg.payload)
return
except ValueError:
_LOGGER.error("Error parsing JSON payload: %s", payload)
_LOGGER.error("Error parsing JSON payload: %s", msg.payload)
return
kwargs = _parse_see_args(dev_id, data)

View File

@@ -11,10 +11,10 @@ import voluptuous as vol
from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA,
DeviceScanner)
from homeassistant.const import (CONF_HOST, CONF_PASSWORD)
from homeassistant.const import (CONF_HOST, CONF_PASSWORD, CONF_SSL)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['quantum-gateway==0.0.3']
REQUIREMENTS = ['quantum-gateway==0.0.5']
_LOGGER = logging.getLogger(__name__)
@@ -22,6 +22,7 @@ DEFAULT_HOST = 'myfiosgateway.com'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_SSL, default=True): cv.boolean,
vol.Required(CONF_PASSWORD): cv.string
})
@@ -42,10 +43,12 @@ class QuantumGatewayDeviceScanner(DeviceScanner):
self.host = config[CONF_HOST]
self.password = config[CONF_PASSWORD]
self.use_https = config[CONF_SSL]
_LOGGER.debug('Initializing')
try:
self.quantum = QuantumGatewayScanner(self.host, self.password)
self.quantum = QuantumGatewayScanner(self.host, self.password,
self.use_https)
self.success_init = self.quantum.success_init
except RequestException:
self.success_init = False

View File

@@ -18,7 +18,7 @@ from homeassistant.util import slugify
from homeassistant.util.json import load_json, save_json
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pytile==2.0.5']
REQUIREMENTS = ['pytile==2.0.6']
CLIENT_UUID_CONFIG_FILE = '.tile.conf'
DEVICE_TYPES = ['PHONE', 'TILE']

View File

@@ -43,6 +43,8 @@ AVAILABLE_ATTRS = [
'uptime', 'user_id', 'usergroup_id', 'vlan'
]
TIMESTAMP_ATTRS = ['first_seen', 'last_seen', 'latest_assoc_time']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_SITE_ID, default='default'): cv.string,
@@ -149,7 +151,12 @@ class UnifiScanner(DeviceScanner):
attributes = {}
for variable in self._monitored_conditions:
if variable in client:
attributes[variable] = client[variable]
if variable in TIMESTAMP_ATTRS:
attributes[variable] = dt_util.utc_from_timestamp(
float(client[variable])
)
else:
attributes[variable] = client[variable]
_LOGGER.debug("Device mac %s attributes %s", device, attributes)
return attributes

View File

@@ -0,0 +1,58 @@
"""Support for device tracking via Xfinity Gateways."""
import logging
from requests.exceptions import RequestException
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
REQUIREMENTS = ['xfinity-gateway==0.0.4']
_LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = '10.0.0.1'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string
})
def get_scanner(hass, config):
"""Validate the configuration and return an Xfinity Gateway scanner."""
from xfinity_gateway import XfinityGateway
gateway = XfinityGateway(config[DOMAIN][CONF_HOST])
scanner = None
try:
gateway.scan_devices()
scanner = XfinityDeviceScanner(gateway)
except (RequestException, ValueError):
_LOGGER.error("Error communicating with Xfinity Gateway. "
"Check host: %s", gateway.host)
return scanner
class XfinityDeviceScanner(DeviceScanner):
"""This class queries an Xfinity Gateway."""
def __init__(self, gateway):
"""Initialize the scanner."""
self.gateway = gateway
def scan_devices(self):
"""Scan for new devices and return a list of found MACs."""
connected_devices = []
try:
connected_devices = self.gateway.scan_devices()
except (RequestException, ValueError):
_LOGGER.error("Unable to scan devices. "
"Check connection to gateway")
return connected_devices
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
return self.gateway.get_device_name(device)

View File

@@ -9,7 +9,6 @@ loaded before the EVENT_PLATFORM_DISCOVERED is fired.
import json
from datetime import timedelta
import logging
import os
import voluptuous as vol
@@ -21,7 +20,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==2.3.0']
REQUIREMENTS = ['netdisco==2.5.0']
DOMAIN = 'discovery'
@@ -31,6 +30,7 @@ SERVICE_AXIS = 'axis'
SERVICE_DAIKIN = 'daikin'
SERVICE_DECONZ = 'deconz'
SERVICE_DLNA_DMR = 'dlna_dmr'
SERVICE_ENIGMA2 = 'enigma2'
SERVICE_FREEBOX = 'freebox'
SERVICE_HASS_IOS_APP = 'hass_ios'
SERVICE_HASSIO = 'hassio'
@@ -39,6 +39,7 @@ SERVICE_HUE = 'philips_hue'
SERVICE_IGD = 'igd'
SERVICE_IKEA_TRADFRI = 'ikea_tradfri'
SERVICE_KONNECTED = 'konnected'
SERVICE_MOBILE_APP = 'hass_mobile_app'
SERVICE_NETGEAR = 'netgear_router'
SERVICE_OCTOPRINT = 'octoprint'
SERVICE_ROKU = 'roku'
@@ -62,12 +63,14 @@ CONFIG_ENTRY_HANDLERS = {
}
SERVICE_HANDLERS = {
SERVICE_MOBILE_APP: ('mobile_app', None),
SERVICE_HASS_IOS_APP: ('ios', None),
SERVICE_NETGEAR: ('device_tracker', None),
SERVICE_WEMO: ('wemo', None),
SERVICE_HASSIO: ('hassio', None),
SERVICE_AXIS: ('axis', None),
SERVICE_APPLE_TV: ('apple_tv', None),
SERVICE_ENIGMA2: ('media_player', 'enigma2'),
SERVICE_ROKU: ('roku', None),
SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
@@ -93,7 +96,7 @@ SERVICE_HANDLERS = {
'kodi': ('media_player', 'kodi'),
'volumio': ('media_player', 'volumio'),
'lg_smart_device': ('media_player', 'lg_soundbar'),
'nanoleaf_aurora': ('light', 'nanoleaf_aurora'),
'nanoleaf_aurora': ('light', 'nanoleaf'),
}
OPTIONAL_SERVICE_HANDLERS = {
@@ -195,10 +198,6 @@ async def async_setup(hass, config):
"""Schedule the first discovery when Home Assistant starts up."""
async_track_point_in_utc_time(hass, scan_devices, dt_util.utcnow())
# Discovery for local services
if 'HASSIO' in os.environ:
hass.async_create_task(new_service_found(SERVICE_HASSIO, {}))
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, schedule_first)
return True

View File

@@ -0,0 +1,6 @@
{
"state": {
"day": "Dag",
"night": "Nacht"
}
}

View File

@@ -0,0 +1,5 @@
{
"state": {
"night": "\u0e01\u0e25\u0e32\u0e07\u0e04\u0e37\u0e19"
}
}

View File

@@ -1,4 +1,6 @@
"""Constants for ebus component."""
from homeassistant.const import ENERGY_KILO_WATT_HOUR
DOMAIN = 'ebusd'
# SensorTypes:
@@ -67,9 +69,9 @@ SENSOR_TYPES = {
'ContinuosHeating':
['ContinuosHeating', '°C', 'mdi:weather-snowy', 0],
'PowerEnergyConsumptionLastMonth':
['PrEnergySumHcLastMonth', 'kWh', 'mdi:flash', 0],
['PrEnergySumHcLastMonth', ENERGY_KILO_WATT_HOUR, 'mdi:flash', 0],
'PowerEnergyConsumptionThisMonth':
['PrEnergySumHcThisMonth', 'kWh', 'mdi:flash', 0]
['PrEnergySumHcThisMonth', ENERGY_KILO_WATT_HOUR, 'mdi:flash', 0]
},
'ehp': {
'HWTemperature':
@@ -89,12 +91,12 @@ SENSOR_TYPES = {
'Flame':
['Flame', None, 'mdi:toggle-switch', 2],
'PowerEnergyConsumptionHeatingCircuit':
['PrEnergySumHc1', 'kWh', 'mdi:flash', 0],
['PrEnergySumHc1', ENERGY_KILO_WATT_HOUR, 'mdi:flash', 0],
'PowerEnergyConsumptionHotWaterCircuit':
['PrEnergySumHwc1', 'kWh', 'mdi:flash', 0],
['PrEnergySumHwc1', ENERGY_KILO_WATT_HOUR, 'mdi:flash', 0],
'RoomThermostat':
['DCRoomthermostat', None, 'mdi:toggle-switch', 2],
'HeatingPartLoad':
['PartloadHcKW', 'kWh', 'mdi:flash', 0]
['PartloadHcKW', ENERGY_KILO_WATT_HOUR, 'mdi:flash', 0]
}
}

View File

@@ -2,6 +2,7 @@
import logging
from homeassistant.helpers.entity import Entity
from homeassistant.const import POWER_WATT
from homeassistant.components.edp_redy import EdpRedyDevice, EDP_REDY
@@ -29,7 +30,7 @@ async def async_setup_platform(
# Create a sensor for global active power
devices.append(EdpRedySensor(session, ACTIVE_POWER_ID, "Power Home",
'mdi:flash', 'W'))
'mdi:flash', POWER_WATT))
async_add_entities(devices, True)
@@ -89,7 +90,7 @@ class EdpRedyModuleSensor(EdpRedyDevice, Entity):
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this sensor."""
return 'W'
return POWER_WATT
async def async_update(self):
"""Parse the data for this sensor."""

View File

@@ -5,13 +5,16 @@ from aiohttp import web
from homeassistant import core
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_SET,
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, STATE_ON, STATE_OFF,
HTTP_BAD_REQUEST, HTTP_NOT_FOUND, ATTR_SUPPORTED_FEATURES,
ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_VOLUME_SET, SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, STATE_ON,
STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, ATTR_SUPPORTED_FEATURES
)
from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS
)
from homeassistant.components.climate.const import (
SERVICE_SET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE
)
from homeassistant.components.media_player.const import (
ATTR_MEDIA_VOLUME_LEVEL, SUPPORT_VOLUME_SET,
)
@@ -26,7 +29,7 @@ from homeassistant.components.cover import (
)
from homeassistant.components import (
cover, fan, media_player, light, script, scene
climate, cover, fan, media_player, light, script, scene
)
from homeassistant.components.http import HomeAssistantView
@@ -262,6 +265,18 @@ class HueOneLightChangeView(HomeAssistantView):
if brightness is not None:
data['variables']['requested_level'] = brightness
# If the requested entity is a climate, set the temperature
elif entity.domain == climate.DOMAIN:
# We don't support turning climate devices on or off,
# only setting the temperature
service = None
if entity_features & SUPPORT_TARGET_TEMPERATURE:
if brightness is not None:
domain = entity.domain
service = SERVICE_SET_TEMPERATURE
data[ATTR_TEMPERATURE] = brightness
# If the requested entity is a media player, convert to volume
elif entity.domain == media_player.DOMAIN:
if entity_features & SUPPORT_VOLUME_SET:
@@ -318,8 +333,9 @@ class HueOneLightChangeView(HomeAssistantView):
core.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id},
blocking=True))
hass.async_create_task(hass.services.async_call(
domain, service, data, blocking=True))
if service is not None:
hass.async_create_task(hass.services.async_call(
domain, service, data, blocking=True))
json_response = \
[create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
@@ -371,7 +387,7 @@ def parse_hue_api_put_light_body(request_json, entity):
elif entity.domain in [
script.DOMAIN, media_player.DOMAIN,
fan.DOMAIN, cover.DOMAIN]:
fan.DOMAIN, cover.DOMAIN, climate.DOMAIN]:
# Convert 0-255 to 0-100
level = brightness / 255 * 100
brightness = round(level)
@@ -397,6 +413,10 @@ def get_entity_state(config, entity):
if entity_features & SUPPORT_BRIGHTNESS:
pass
elif entity.domain == climate.DOMAIN:
temperature = entity.attributes.get(ATTR_TEMPERATURE, 0)
# Convert 0-100 to 0-255
final_brightness = round(temperature * 255 / 100)
elif entity.domain == media_player.DOMAIN:
level = entity.attributes.get(
ATTR_MEDIA_VOLUME_LEVEL, 1.0 if final_state else 0.0)

View File

@@ -0,0 +1,12 @@
{
"config": {
"step": {
"user": {
"data": {
"host_ip": "\u0e42\u0e2e\u0e2a\u0e15\u0e4c IP",
"name": "\u0e0a\u0e37\u0e48\u0e2d"
}
}
}
}
}

View File

@@ -6,7 +6,9 @@
"step": {
"user": {
"data": {
"host_ip": "\u4e3b\u673aIP",
"advertise_ip": "\u5e7f\u64ad IP",
"advertise_port": "\u5e7f\u64ad\u7aef\u53e3",
"host_ip": "\u4e3b\u673a IP",
"listen_port": "\u76d1\u542c\u7aef\u53e3",
"name": "\u59d3\u540d"
},

View File

@@ -0,0 +1 @@
"""Support for Enigma2 devices."""

View File

@@ -0,0 +1,225 @@
"""Support for Enigma2 media players."""
import logging
import voluptuous as vol
from homeassistant.components.media_player import MediaPlayerDevice
from homeassistant.helpers.config_validation import (PLATFORM_SCHEMA)
from homeassistant.components.media_player.const import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_ON,
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_STEP, MEDIA_TYPE_TVSHOW)
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_SSL,
STATE_OFF, STATE_ON, STATE_PLAYING, CONF_PORT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['openwebifpy==1.2.7']
_LOGGER = logging.getLogger(__name__)
ATTR_MEDIA_CURRENTLY_RECORDING = 'media_currently_recording'
ATTR_MEDIA_DESCRIPTION = 'media_description'
ATTR_MEDIA_END_TIME = 'media_end_time'
ATTR_MEDIA_START_TIME = 'media_start_time'
CONF_USE_CHANNEL_ICON = "use_channel_icon"
DEFAULT_NAME = 'Enigma2 Media Player'
DEFAULT_PORT = 80
DEFAULT_SSL = False
DEFAULT_USE_CHANNEL_ICON = False
DEFAULT_USERNAME = 'root'
DEFAULT_PASSWORD = 'dreambox'
SUPPORTED_ENIGMA2 = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_OFF | SUPPORT_NEXT_TRACK | SUPPORT_STOP | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_VOLUME_STEP | \
SUPPORT_TURN_ON | SUPPORT_PAUSE | SUPPORT_SELECT_SOURCE
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_USE_CHANNEL_ICON,
default=DEFAULT_USE_CHANNEL_ICON): cv.boolean,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up of an enigma2 media player."""
if discovery_info:
# Discovery gives us the streaming service port (8001)
# which is not useful as OpenWebif never runs on that port.
# So use the default port instead.
config[CONF_PORT] = DEFAULT_PORT
config[CONF_NAME] = discovery_info['hostname']
config[CONF_HOST] = discovery_info['host']
config[CONF_USERNAME] = DEFAULT_USERNAME
config[CONF_PASSWORD] = DEFAULT_PASSWORD
config[CONF_SSL] = DEFAULT_SSL
config[CONF_USE_CHANNEL_ICON] = DEFAULT_USE_CHANNEL_ICON
from openwebif.api import CreateDevice
device = \
CreateDevice(host=config[CONF_HOST],
port=config.get(CONF_PORT),
username=config.get(CONF_USERNAME),
password=config.get(CONF_PASSWORD),
is_https=config.get(CONF_SSL),
prefer_picon=config.get(CONF_USE_CHANNEL_ICON))
add_devices([Enigma2Device(config[CONF_NAME], device)], True)
class Enigma2Device(MediaPlayerDevice):
"""Representation of an Enigma2 box."""
def __init__(self, name, device):
"""Initialize the Enigma2 device."""
self._name = name
self.e2_box = device
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
if self.e2_box.is_recording_playback:
return STATE_PLAYING
return STATE_OFF if self.e2_box.in_standby else STATE_ON
@property
def supported_features(self):
"""Flag of media commands that are supported."""
return SUPPORTED_ENIGMA2
def turn_off(self):
"""Turn off media player."""
self.e2_box.turn_off()
def turn_on(self):
"""Turn the media player on."""
self.e2_box.turn_on()
@property
def media_title(self):
"""Title of current playing media."""
return self.e2_box.current_service_channel_name
@property
def media_series_title(self):
"""Return the title of current episode of TV show."""
return self.e2_box.current_programme_name
@property
def media_channel(self):
"""Channel of current playing media."""
return self.e2_box.current_service_channel_name
@property
def media_content_id(self):
"""Service Ref of current playing media."""
return self.e2_box.current_service_ref
@property
def media_content_type(self):
"""Type of video currently playing."""
return MEDIA_TYPE_TVSHOW
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self.e2_box.muted
@property
def media_image_url(self):
"""Picon url for the channel."""
return self.e2_box.picon_url
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
self.e2_box.set_volume(int(volume * 100))
def volume_up(self):
"""Volume up the media player."""
self.e2_box.set_volume(int(self.e2_box.volume * 100) + 5)
def volume_down(self):
"""Volume down media player."""
self.e2_box.set_volume(int(self.e2_box.volume * 100) - 5)
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return self.e2_box.volume
def media_stop(self):
"""Send stop command."""
self.e2_box.set_stop()
def media_play(self):
"""Play media."""
self.e2_box.toggle_play_pause()
def media_pause(self):
"""Pause the media player."""
self.e2_box.toggle_play_pause()
def media_next_track(self):
"""Send next track command."""
self.e2_box.set_channel_up()
def media_previous_track(self):
"""Send next track command."""
self.e2_box.set_channel_down()
def mute_volume(self, mute):
"""Mute or unmute."""
self.e2_box.mute_volume()
@property
def source(self):
"""Return the current input source."""
return self.e2_box.current_service_channel_name
@property
def source_list(self):
"""List of available input sources."""
return self.e2_box.source_list
def select_source(self, source):
"""Select input source."""
self.e2_box.select_source(self.e2_box.sources[source])
def update(self):
"""Update state of the media_player."""
self.e2_box.update()
@property
def device_state_attributes(self):
"""Return device specific state attributes.
isRecording: Is the box currently recording.
currservice_fulldescription: Full program description.
currservice_begin: is in the format '21:00'.
currservice_end: is in the format '21:00'.
"""
attributes = {}
if not self.e2_box.in_standby:
attributes[ATTR_MEDIA_CURRENTLY_RECORDING] = \
self.e2_box.status_info['isRecording']
attributes[ATTR_MEDIA_DESCRIPTION] = \
self.e2_box.status_info['currservice_fulldescription']
attributes[ATTR_MEDIA_START_TIME] = \
self.e2_box.status_info['currservice_begin']
attributes[ATTR_MEDIA_END_TIME] = \
self.e2_box.status_info['currservice_end']
return attributes

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