Compare commits

...

262 Commits

Author SHA1 Message Date
Pascal Vizeli
09292d5918 Update azure-pipelines-release.yml for Azure Pipelines 2019-06-05 22:15:18 +02:00
Pascal Vizeli
d78e132007 Merge pull request #24305 from home-assistant/rc
0.94.0
2019-06-05 18:35:04 +02:00
Robert Svensson
8d3c9bc2d0 Don't let zeroconf be smart with addresses (#24321) 2019-06-05 18:33:31 +02:00
Pascal Vizeli
ce93a332a7 Update and rename azure-pipelines.yml to azure-pipelines-release.yml 2019-06-05 09:37:57 +02:00
Paulus Schoutsen
13c3833593 Bumped version to 0.94.0 2019-06-04 14:34:06 -07:00
Paulus Schoutsen
d0715c75c0 Merge remote-tracking branch 'origin/master' into rc 2019-06-04 14:33:49 -07:00
Paulus Schoutsen
eca424656a Fix OwnTracks race condition (#24303)
* Fix OwnTracks race condition

* Lint
2019-06-04 14:25:10 -07:00
Robert Svensson
3b60081e2a address is deprecated in favor of addresses (#24302) 2019-06-04 14:25:09 -07:00
Paulus Schoutsen
1096fe3d87 Bumped version to 0.94.0b8 2019-06-04 11:06:25 -07:00
Paulus Schoutsen
389da16947 Upgrade Zeroconf to 0.23 (#24300) 2019-06-04 11:06:19 -07:00
Paulus Schoutsen
185af1b42a Run SSDP discovery in parallel (#24299) 2019-06-04 11:06:18 -07:00
Pascal Vizeli
d17f27b65c Create progress file for pip installs (#24297)
* Create progress file for pip installs

* fix dedlock

* unflacky test

* Address comments

* Lint

* Types
2019-06-04 11:06:18 -07:00
Paulus Schoutsen
bb0867f1a8 Guard against bad states in Mobile App/OwnTracks (#24292) 2019-06-04 11:06:17 -07:00
Paulus Schoutsen
b67d32824c Updated frontend to 20190604.0 2019-06-04 09:41:07 -07:00
Pascal Vizeli
bad920fa87 Bumped version to 0.94.0b7 2019-06-04 12:42:45 +02:00
Paulus Schoutsen
281fe93a26 Bumped version to 0.94.0b6 2019-06-03 12:41:45 -07:00
Paulus Schoutsen
4a71593ffd Remove deps folder in config when on Docker (#24284)
* Remove deps folder in config

* Fix tests

* Fix tests with docker check
2019-06-03 12:41:36 -07:00
Paulus Schoutsen
014cc14b7e Fix cors on the index view (#24283) 2019-06-03 12:41:35 -07:00
Otto Winter
ee71d2ca60 Bump aioesphomeapi to 2.1.0 (#24278)
* Bump aioesphomeapi to 2.1.0

* Update requirements txt
2019-06-03 12:41:34 -07:00
Paul Bottein
5085ce8ab1 Add temperature sensor support to google smarthome thermostat device (#24264)
* Add temperature sensor support to google smarthome thermostat device

* fix lint for trait_test

* Reset temperature unit in tests

* Address comment
2019-06-03 12:41:32 -07:00
Robert Svensson
9ed5b70d01 deCONZ migrate to SSDP discovery (#24252)
* Migrate deCONZ to use new SSDP discovery
Add new discovery info manufacturer URL to be able to separate Hue and deCONZ bridges

* Mark deCONZ as migrated in Discovery component

* Fix tests

* Fix Hue discovery ignore deCONZ bridge

* Less snake more badger

* Mushroom

* Fix indentation

* Config flow ignore manufacturer url that is not philips
2019-06-03 12:41:31 -07:00
Pascal Vizeli
a00d8a493d Update azure-pipelines.yml for Azure Pipelines 2019-06-03 12:31:31 +02:00
Pascal Vizeli
2b0e56932b Update azure-pipelines.yml for Azure Pipelines 2019-06-03 11:51:34 +02:00
Pascal Vizeli
704cdac874 Bumped version to 0.94.0b5 2019-06-03 08:36:38 +00:00
Paulus Schoutsen
89d7c0af91 Add restore state to Geofency (#24268)
* Add restore state to Geofency

* Lint
2019-06-03 08:35:44 +00:00
Paulus Schoutsen
5f3bcedbba Mobile app device tracker to restore state (#24266) 2019-06-03 08:35:43 +00:00
Paulus Schoutsen
d2d3f27f85 Add restore state to OwnTracks device tracker (#24256)
* Add restore state to OwnTracks device tracker

* Lint

* Also store entity devices

* Update test_device_tracker.py
2019-06-03 08:35:43 +00:00
Paulus Schoutsen
a8c73ffb93 Updated frontend to 20190602.0 2019-06-02 14:00:52 -07:00
Paulus Schoutsen
22f68d70a7 Bumped version to 0.94.0b4 2019-06-01 14:34:39 -07:00
Paulus Schoutsen
bf85e18d45 Do not use the cache dir for PIP installs (#24233) 2019-06-01 14:34:27 -07:00
Paulus Schoutsen
09c43e8854 Updated frontend to 20190601.0 2019-06-01 14:34:06 -07:00
Paulus Schoutsen
e5cbf01ce1 Bumped version to 0.94.0b3 2019-05-31 23:05:57 -07:00
Paulus Schoutsen
fe2e5089ab Mobile app to use device tracker config entry (#24238)
* Mobile app to use device tracker config entry

* Lint

* Re-use device_info

* Lint
2019-05-31 23:05:36 -07:00
Teemu R
35ffac1e01 add a deprecation warning for tplink device_tracker (#24236)
* add a deprecation warning for tplink device_tracker

* reword the warning a bit
2019-05-31 23:05:35 -07:00
Paulus Schoutsen
362f23a950 GeoFency unique ID and device info (#24232) 2019-05-31 23:05:35 -07:00
Paulus Schoutsen
dc8d4ac8e4 Add GPSLogger device_info and unique_id (#24231) 2019-05-31 23:05:34 -07:00
Robert Svensson
0cdea28e2a Don't allow more than one config flow per discovered Axis device (#24230) 2019-05-31 23:05:33 -07:00
Paulus Schoutsen
7d1a02feb1 Log HomeKit model (#24229) 2019-05-31 23:05:32 -07:00
Pascal Vizeli
958b894020 Update azure-pipelines.yml for Azure Pipelines 2019-05-31 23:22:37 +02:00
Paulus Schoutsen
5c8f209aa7 Bumped version to 0.94.0b2 2019-05-31 13:45:41 -07:00
Paulus Schoutsen
3eeccc1a65 Add manifest support for homekit discovery (#24225)
* Add manifest support for homekit discovery

* Add a space after model check

* Update comment
2019-05-31 13:40:36 -07:00
Paulus Schoutsen
52e33c2aa2 Use resource for index routing. (#24223) 2019-05-31 13:40:36 -07:00
Pascal Vizeli
35f5784287 Don't follow redirect on ingress itself (#24218)
* Don't follow redirect on ingress itself

* Fix comment
2019-05-31 13:40:35 -07:00
Robert Svensson
46cc6e199b Axis - Handle Vapix error messages (#24215) 2019-05-31 13:40:34 -07:00
Paulus Schoutsen
6371eca14d Improve error handling (#24204) 2019-05-31 13:40:34 -07:00
Paulus Schoutsen
052641e620 Instantiate lock inside event loop (#24203) 2019-05-31 13:40:33 -07:00
Paulus Schoutsen
16edcd9938 Allow discovery flows to be discovered via zeroconf/ssdp (#24199) 2019-05-31 13:40:32 -07:00
Thomas Hervé
4fa6f2e54f Bump oauthlib version (#24111)
* Bump oauthlib version

* Bump fitbit instead

* Update requirements
2019-05-31 13:40:31 -07:00
Otto Winter
9be1b72ed7 Fix ESPHome config flow with invalid config entry (#24213) 2019-05-31 11:33:31 +02:00
Pascal Vizeli
bfc8d2457c Update azure-pipelines.yml for Azure Pipelines 2019-05-31 10:57:53 +02:00
Paulus Schoutsen
1ad495070d Bumped version to 0.94.0b1 2019-05-30 14:59:14 -07:00
Paulus Schoutsen
84719d944a Update hass-nabucasa (#24197) 2019-05-30 14:59:08 -07:00
Jc2k
4ca588deae homekit_controller no longer logs with transient network errors causing crypto failures as it will auto recover (#24193) 2019-05-30 14:59:07 -07:00
Otto Winter
325001933d Fix ESPHome discovered when already exists (#24187)
* Fix ESPHome discovered when already exists

* Update .coveragerc
2019-05-30 14:59:07 -07:00
Paulus Schoutsen
acc9fd0382 Dynamic panels (#24184)
* Don't require all panel urls to be registered

* Allow removing panels, fire event when panels updated
2019-05-30 14:59:06 -07:00
Pascal Vizeli
ca89d6184c Update azure-pipelines.yml for Azure Pipelines 2019-05-30 18:50:47 +02:00
Pascal Vizeli
2bfe7aa219 Update azure-pipelines.yml for check version (#24194) 2019-05-30 18:50:32 +02:00
Paulus Schoutsen
78ffb6f3e6 Updated frontend to 20190530.0 2019-05-30 09:32:37 -07:00
Paulus Schoutsen
c08862679d Bumped version to 0.94.0b0 2019-05-29 16:01:51 -07:00
Andre Lengwenus
50db622689 Add service calls for LCN component (#24105) 2019-05-29 15:59:38 -07:00
Jc2k
9303a56d8f Fix duplicated discovered homekit devices (#24178) 2019-05-29 15:49:59 -07:00
Paulus Schoutsen
6667138b73 Remove discovery from initial config (#24183) 2019-05-29 15:32:36 -07:00
Pascal Vizeli
d9852bc75d Support Hass.io wheels / docker env (#24175)
* Support Hass.io wheels / docker env

* address comments

* fix lint
2019-05-29 15:30:09 -07:00
Paulus Schoutsen
6aeccf0330 Merge remote-tracking branch 'origin/master' into dev 2019-05-29 15:16:05 -07:00
Anders Melchiorsen
f8572c1d71 Avoid slow updates with unavailable Sonos devices (#24180) 2019-05-29 15:05:12 -07:00
Robert Svensson
e2e001d042 Keep integrations in discovery (#24179)
* Keep integrations that have been migrated to new discovery methods to avoid breaking changes

* Additional migrated services
2019-05-29 14:34:44 -07:00
Paulus Schoutsen
e3307213b1 Deprecate Python 3.5.3 (#24177) 2019-05-29 14:30:00 -07:00
Robert Svensson
84baaa324c Revert Zeroconf back to previously used library (#24139)
* Revert back to previously used library

* Fix test

* Remove unused import

* Fix import

* Update __init__.py

* Update __init__.py

* Fix test after rebase
2019-05-29 14:20:06 -07:00
Robert Svensson
42ee8eef50 Move Homekit controller component to user zeroconf discovery (#24042) 2019-05-29 11:20:04 -07:00
Robert Svensson
3fef9a93cf Trådfri component to use new zeroconf discovery (#24041)
* Move tradfri component to use new zeroconf discovery

* Will this work?

* Remove prints

* Correct order in generated zeroconf

* Update test_init.py

* Update test_init.py

* Update test_init.py

* Update test_init.py
2019-05-29 11:19:50 -07:00
Paulus Schoutsen
4b256f3466 Reinstate passing loop to DSMR (#24127)
* Reinstate passing loop

* Also pass loop into other part
2019-05-29 11:13:29 -07:00
Anders Melchiorsen
bebfc3d16e Remove unused Sonos turn on/off methods (#24174) 2019-05-29 11:12:44 -07:00
Paulus Schoutsen
fd3902f7e7 update translations 2019-05-29 10:16:58 -07:00
Paulus Schoutsen
dfb992adb2 Updated frontend to 20190529.0 2019-05-29 09:41:35 -07:00
Robbie Trencheny
a252065f99 Fix calling notify.notify with mobile_app targets in play. Fixes #24064 (#24156) 2019-05-29 09:09:24 -07:00
Paulus Schoutsen
6947f8cb2e Cloud: Websocket API to manage Google assistant entity config (#24153)
* Extract exposed devices function

* Add might_2fa info to trait

* Do not filter with should_expose in Google helper func

* Cloud: allow setting if Google entity is exposed

* Allow disabling 2FA via config

* Cloud: allow disabling 2FA

* Lint

* More changes

* Fix typing
2019-05-29 08:39:12 -07:00
Morten Trab
85dfea1642 Add Repetier-Server Component (#21658)
* Bumped to version 2.0

* Updated requirements

* Updated requirements and coveragerc

* Removed long lines, changes to coveragerc and requirements

* Fixed under-indented lines

* Fixed invalid syntax

* Updated .coveragerc to include repetier/__init__.py and sensor.py

* Module update

* Rebased to latest dev

* Blank lines fix

* Add missing manifest.json

* Update requirements

* Bumped component to new API module style

* Removed whitespaces and line feeds

* Added missing newline

* Added missing heated chamber sensor

* Fixed wrong indentation

* Various fixes

* Various build fixes

* Clean up

* Load platform only once

* Sort imports

* Add printer api

* Clean up

* Build out sensor classes

* Clarify temperature sensor variable names

* Move constants

* Clean up name

* Run script/gen_requirements_all.py

* Working code, missing auto add of new sensors

* Updated code to return proper device_class and timestamp

* Removed unnessecary code

* Renamed elapsed and remaining sensors

* Dynamically adding sensors as they become available

* Rebased .coveragerc due to conflicts

* Code changes and cleanup

* Removed whitespace and code simplification
2019-05-29 08:31:04 -04:00
Otto Winter
015c8811a5 Use global imports for ESPHome (#24158)
* Use global import for ESPHome

* Add aioesphomeapi to test requirements

aioesphomeapi is also shipped as a pure-python wheel, so this should not impact test install time
2019-05-29 13:33:49 +02:00
Robbie Trencheny
d9c78b77cb Use device name for device_tracker entry (#24155) 2019-05-28 19:52:47 -07:00
chmielowiec
1b543cf538 Upgrade huawei-lte-api to 1.2.0 (#24165) 2019-05-28 19:51:07 -07:00
Alexei Chetroi
9fb8144031 Debug log when polling ZHA light. (#24167) 2019-05-28 19:50:48 -07:00
William Scanlon
f2033c418f Pubnub to 1.0.6 (#24159) 2019-05-28 12:09:30 -04:00
William Scanlon
aa266cb630 pubnubsub-handler to 1.0.5 (#24154) 2019-05-27 22:09:05 -04:00
Sylvia van Os
9a5d783537 Don't crash on first EAN without installations (#24137)
* Don't crash on first EAN without installations

* Remove duplicated values

* Switch from Exception to persistent notification

* Make pylint happy
2019-05-27 22:36:15 +02:00
Jesse Rizzo
5800b57791 bump dependency envoy_reader to 0.4 (#24145)
* bump envoy_reader version to 0.4

* bump dependency envoy_reader to 0.4
2019-05-27 19:48:00 +02:00
maheus
c840771c0a Add station name for creating the unique_id in netatmo platform (#24141) 2019-05-27 18:07:59 +02:00
Julien Brochet
9678752480 Retrieve wire and wireless devices with the SRM device tracker (#24117) 2019-05-27 18:00:21 +02:00
Robert Svensson
31b2f331db Library refactorization of deCONZ (#23725)
* Improved sensors

* Lib update signalling

* Replace reason with changed

* Move imports to top of file

* Add support for secondary temperature reported by some Xiaomi devices

* Bump dependency to v59
2019-05-27 06:56:00 +02:00
jjlawren
0ba54ee9b7 Use central polling to update entities (#24059)
* Use central polling to update entities

* Fix for line length

* Remove unnecessary import

* Use interval

* Trigger entity refreshes after commands

* Lint
2019-05-26 21:39:50 -07:00
Bram Kragten
5c86a51b45 Lovelace: Fire event on save (#24104)
* Lovelace: Fire event on save

* Add event to whitelist
2019-05-26 20:27:07 -07:00
Paulus Schoutsen
9debbfb1a8 Add SSDP integration (#24090)
* Add SSDP integration

* Fix tests

* Sort all the things

* Add netdisco to test requirements
2019-05-26 19:48:27 -07:00
Anders Melchiorsen
97b671171b Avoid useless Sonos state updates (#24135) 2019-05-26 12:49:26 -07:00
Paulus Schoutsen
179fb0f3b5 Use importlib metadata to check installed packages (#24114)
* Use importlib metadata

* Fix script

* Remove unused import

* Update requirements"
2019-05-26 11:58:42 -07:00
David Bonnes
96b7bb625d geniushub: fix sensor battery level, and bump client (#24123)
* Initial commit

* bump client
2019-05-26 14:01:29 +02:00
Eduard van Valkenburg
afeb13d980 Azure Event Hub history component (#23878)
* v1 of Azure Event Hub History component

* updates to EH

* small fix

* small updates and changed requirements_all

* new version of Event Hub component

* redid config to just ask names

* small edit

* latest version of EH component

* updated codeowners

* codeowner fix

* typo in domain

* updates based on reviews.

* using built-in jsonencoder for DT

* delete unused import
2019-05-26 13:55:40 +02:00
jgriff2
6e1728542e Add Remote RPi Component (#23518)
* Add Remote RPi Component

* Add Remote RPi Component

* fix imports

* Added support for setup as switch and binary_sensor

* remove pylint error handling

* Changed to domain config

* Changed to domain config

* Changed to domain config

* Changed to domain config

* Update __init__.py

* Update manifest.json

* Update requirements_all.txt

* Update switch.py

* Update binary_sensor.py

* Changed to domain config
2019-05-26 13:52:06 +02:00
Otto Winter
9438dd1cbd Use name in ESPHome discovery title (#24100)
* Use name in ESPHome discovery title

* Add test

* Lint
2019-05-26 13:48:05 +02:00
Andrew Sayre
0194905e97 Move imports to top (#24108) 2019-05-26 13:47:11 +02:00
jjlawren
25505dc1d4 Remove custom entity_id naming (#24072)
* Remove custom entity_id naming

* Set entity_ids with 'plex'

* Set name instead of entity_id

* Lint

* Use a name template
2019-05-26 12:28:29 +02:00
Jardi Martinez
ce219ac6c7 Set assumed_state property to True. (#24118) 2019-05-26 12:26:51 +02:00
cgtobi
fa20957e01 Bump pyatmo version to 1.12 (#24088) 2019-05-26 12:09:02 +02:00
Robin Wohlers-Reichel
7959c04d1e Solax Inverter Sensor Component (#22579)
* Solax inverter direct API

* Linter compliance

* lint++

* move api communication to external lib

* lint++

* requirements

* Revert "requirements"

This reverts commit 82a6c0c095.

* potentially?

* Addressing review comments

* Also update CODEOWNERS

* Only update sensor state if data has changed
2019-05-25 21:55:30 -05:00
Paulus Schoutsen
e6d7f6ed71 Config entry device tracker (#24040)
* Move zone helpers to zone root

* Add config entry support to device tracker

* Convert Geofency

* Convert GPSLogger

* Track unsub per entry

* Convert locative

* Migrate OwnTracks

* Lint

* location -> latitude, longitude props

* Lint

* lint

* Fix test
2019-05-25 13:34:53 -07:00
Alex Bahm
144b530045 Issue #23514 - fix invalid hue response (#23909)
Based on the discoveries in issue #23514, the periodic lack of response from emulated hue was due to an invalid value (null) being returned.
2019-05-25 13:07:23 -07:00
Kevin Fronczak
39ba99005a Fix broken blink motion detection (#24097) 2019-05-25 17:58:44 +02:00
Simon Nørager Sørensen
f867b025e5 Update code owner for Xiaomi TV (#24102)
* Update code owner

* Update CODEOWNERS
2019-05-25 17:57:16 +02:00
Andre Lengwenus
c928f82cbf Refactoring of LCN component (#23824)
* Moved helper functions to const.py

* Removed pypck attribute from LcnDevice

* Bump to pypck==0.6.0

* Added myself as a codeowner

* Moved helper functions to helpers.py
2019-05-25 11:40:44 +02:00
Joakim Plate
9d7aa8f05d Remove device tracker unnecessary separate except clause (#24081)
Handle exception where it can be thrown.
2019-05-25 11:29:19 +02:00
Daniel Høyer Iversen
02f927ae2d typo for ambiclimate (#24083) 2019-05-25 09:26:46 +02:00
Jardi Martinez
1d022522cd MCP23017 (#23127)
* Added support for MCP23017 I2C GPIO extender.

* Updated .coveragerc to exclude mcp23017 component from tests.

* Generated CODEOWNERS for mcp23017 usign script.

* Removed .svn folder that had been accidentally uploaded.

* Added link to www.home-assistant.io docs in manifest.json

* Fixed logic error in switch platform.

* Cleaned up code and removed unnecessary should_poll() function.

* Limited the options for pull mode to UP and DOWN

* Fixed line too long in binary sensor.

* Fixed line too long on switch.py

* Changed to setup_platform.

* Reorder constants
2019-05-25 08:09:53 +02:00
terual
bad9ac5395 Fix Hue bridge timeout (#24084)
* Change timeout from 5 seconds to 10 seconds
Underpowered platforms timeout during configuration/discovery of a Hue bridge on a new install. Increasing this timeout fixes this.
2019-05-24 15:55:13 -07:00
Joakim Plate
e9f561e7ab Adjust logging (#24082)
* Make sure we log full path to debug log

* Make sure we log the exception to debug log
2019-05-24 15:54:04 -07:00
Jeff Irion
14d169558f Add 'adb_response' attribute to Android TV / Fire TV (#23960)
* Add 'adb_response' attribute to Android TV / Fire TV

* Use None instead of empty string for empty ADB responses

* Initialize self._adb_response as None, not empty string

* Update the state after sending an ADB command

This ensures that the `'adb_response'` attribute contains the response to the latest command
2019-05-24 18:43:35 -04:00
P0L0
ca2a68217d Added possibility to define the data type of Homematic (#24078)
* Homematic: Added possibility to define the data type for set_device_value

* Fixed coding style

* Fixed variable name
2019-05-24 17:28:45 +02:00
dreed47
0a9a8ecc4e Update the name of Zestimate sensors (#23770)
* Zestimate: fix for issue #23757
Changed name property to return Zestimate
and the property address.  This will make it easier
distinguish multiple Zestimate sensor entities
in the UI.

Also removed MIN_TIME_BETWEEN_UPDATES in
favor of SCAN_INTERVAL per suggestion from
amelchio#9580 on Discord

* Zestimate fix for issue #23757

Changed name property to return Zestimate
and the property address.  This will make it easier
distinguish multiple Zestimate sensor entities
in the UI.

* Changed name property to return Zestimate
and the property address.  This will make it easier
distinguish multiple Zestimate sensor entities
in the UI.

* moved code fix to the correct function

* removed code change from unique_id function
2019-05-24 16:01:55 +02:00
Daniel Høyer Iversen
6cef850497 Rfxtrx, add data types (#24066)
* Rfxtrx, add data types

* fix style
2019-05-24 09:46:59 +02:00
Paulus Schoutsen
66af4bd011 Fix zeroconf sorting (#24068) 2019-05-23 14:41:57 -07:00
Jeff Irion
03253f4598 Better logging of method used for ADB connection (#24037) 2019-05-23 13:57:00 -07:00
Fredrik Erlandsson
aa5d8e5a81 Daikin airbase beta fixes (#24050)
* values are strings not integers

* pydaikin version bump
2019-05-23 13:55:22 -07:00
Paulus Schoutsen
206029eadc Update translations 2019-05-23 13:32:35 -07:00
Paulus Schoutsen
958c5ecbfe Updated frontend to 20190523.0 2019-05-23 13:32:16 -07:00
Troels Agergaard Jacobsen
3d79bf2bfe Fix entity id naming when not using first install (#23606)
* Fix entity id naming when not using first install

Currently, the verisure component will use the alias of the first
installation to decide entity id of the alarm_control_panel even though
a different installation is configured through a specified giid. This
fixes that

* Fixed pulled request review comments

* Remove trailing whitespace

* Fix remaining pylint errors
2019-05-23 19:27:42 +02:00
Paulus Schoutsen
1de0a0bbb9 Convert stream source to method (#23905)
* Convert stream source to method

* Use async with
2019-05-23 09:45:30 -07:00
jjlawren
8d22479d24 Always update all Plex client types (#24038) 2019-05-23 14:00:41 +02:00
Daniel Høyer Iversen
7f7435f003 Add support for available property for broadlink (#23981)
* Add support for available property for broadlink

* Broadlink, except oserror

* Broadlink, except oserror
2019-05-23 09:52:30 +02:00
Pascal Vizeli
d2eb5bb0f3 [skip ci] Update azure-pipelines.yml for Azure Pipelines 2019-05-23 09:46:40 +02:00
Robert Svensson
085303c349 ESPHome component to use zeroconf discovery (#24043)
* Move ESPHome component to use zeroconf discovery

* Remove esphome from discovery component
2019-05-23 08:55:08 +02:00
Daniel Høyer Iversen
9ac6f906ff Update ambiclimate library (#24049) 2019-05-23 08:53:38 +02:00
Paulus Schoutsen
f995ab9d54 Don't pass in loop (#23984)
* Don't pass in loop

* Revert some changes

* Lint + Axis revert

* reinstate loop

* Fix a test

* Set loop

* Update camera.py

* Lint
2019-05-22 21:09:59 -07:00
Paulus Schoutsen
77f595c9a4 Merge pull request #24047 from home-assistant/rc
0.93.2
2019-05-22 20:34:12 -07:00
Paulus Schoutsen
8d0b1588be Bumped version to 0.93.2 2019-05-22 20:00:34 -07:00
Daniel Høyer Iversen
70c5c82541 upgrade broadlink library (#23966) 2019-05-22 20:00:12 -07:00
Cyro
bf910ef383 Make Discord payload data key not required (#23964) 2019-05-22 20:00:11 -07:00
Julien Brochet
99c49c0993 Setup integration dependencies before loading it (#23957) 2019-05-22 20:00:10 -07:00
Joakim Sørensen
f6e6c21ba6 Fixes issue with multiple alerts (#23945)
* Fixes issue with multiple alerts

* Adds missing new line

* Remove whitespace
2019-05-22 20:00:10 -07:00
Joakim Sørensen
41b7f5ab1c Bump pytraccar (#23939) 2019-05-22 20:00:09 -07:00
Pascal Vizeli
c5bd6b3d6b Fix auto version update Hass.io (#23935) 2019-05-22 20:00:08 -07:00
Paulus Schoutsen
9e96397e6a Require core config detection to be triggerd manually (#24019)
* Detect core config

* Remove elevation

* Lint

* Lint

* Fix type
2019-05-22 17:24:46 -07:00
Fabian Affolter
f207e01510 Upgrade Mastodon.py to 1.4.2 (#24004)
* Upgrade Mastodon.py to 1.4.2

* Update
2019-05-22 23:05:03 +02:00
Daniel Høyer Iversen
6b3bb3347b Ambiclimate test, mock (#24034) 2019-05-22 15:00:05 +02:00
Paulus Schoutsen
806903ffe0 Downgrade Hue warning (#24033) 2019-05-22 14:59:16 +02:00
zewelor
fdf1fa48e3 Improve yeelight imports (#24020)
* Improve yeelight imports

* Move import on top

* Fix lint
2019-05-21 22:47:10 -04:00
Robert Svensson
636077c74d Zeroconf discovery for config entries (#23919)
* Proof of concept

* Follow comments

* Fix line length and bad imports

* Move imports to top

* Exception handling for unicode decoding
Create debug print for new service types
Add empty test files

* First try at a test

* Add type and name to service info
Fix static check

* Add aiozeroconf to test dependencies
2019-05-21 15:36:26 -07:00
David Bonnes
e047e4dcff bump geniushub-client to 0.4.9 (#24022) 2019-05-21 15:57:24 -04:00
Tyler Page
eae306c3f1 Fix iterating over NoneType exception (#23648)
* Fix iterating over NoneType exception

When self._dark_sky is None, don't try to return self._dark_sky.units

* Fix wrong check
2019-05-21 08:26:11 -04:00
David Bonnes
fbd7c72283 Add geniushub sensors for issues (#23976)
* Inital commit

* delint - use new string formatting
2019-05-21 08:23:38 -04:00
Erik Montnemery
fc58746bc3 Add websocket API for updating core config (#24009)
* Add websocket API for updating core config
2019-05-21 07:21:31 +02:00
Erik Montnemery
9ae878d8f2 Update CODEOWNERS (#24015) 2019-05-21 07:20:23 +02:00
Erik Montnemery
afe9fc221e Fire event when core config is updated (#23922)
* Fire event when core config is updated
2019-05-20 20:02:36 +02:00
Robert Svensson
eb912be47a Axis IO-port support (#23312)
Support digital inputs and supervised inputs, digital outputs and relays
2019-05-20 07:45:31 +02:00
Paulus Schoutsen
5c346e8fb6 Update owner frontend integrations [skip ci] (#24001) 2019-05-20 05:01:02 +02:00
Greg Dowling
8d388c5e79 Bump loopenergy library version - catches runtime exception. (#23989)
* Bump loopenergy library version - catches runtime exception.

* Update requirements_all.
2019-05-19 12:51:10 -04:00
Fredrik Erlandsson
314574fc84 daikin version bump (#23991) 2019-05-19 12:49:03 -04:00
Paulus Schoutsen
e356d0bcda Better handle file not found when loading YAML (#23908)
* Better handle file not found

* Lint
2019-05-19 12:01:29 +02:00
Penny Wood
f991ec15f2 Delete devices / entities when we remove a config entry. (#23983)
* Remove device when last config entry removed

* Remove entities when config entry removed

* Update tests to use new behaviour
2019-05-19 11:41:39 +02:00
Tomer Figenblat
d7d83c683d Updated non-blocking timout to 10 seconds for fixing timeout issues. (#23930)
* Updated non-blocking timout to 10 seconds for fixing timeout issues.

* Added failed bridge fixture for faster unit tests.
2019-05-19 11:24:59 +02:00
Joakim Sørensen
ff867a7d57 Use the timezone defined in Home Assistant when making the API call (#23284)
* Use HA defined timezone

* Cleanup

* Use homeassistant.util.dt.now to get the correct time.

* Update homeassistant/components/vasttrafik/sensor.py

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>
2019-05-19 11:23:55 +02:00
Charles Garwood
1282370ccb Entity Cleanup on Z-Wave node removal (#23633)
* Initial groundwork for entity cleanup on node removal

* Connect node_removed to dispatcher

* update docstring

* Add node_removal test

* Address review comments

* Use hass.add_job instead of run_coroutine_threadsafe
2019-05-19 11:14:11 +02:00
András Rutkai
eebd094423 Adding Watson TTS (IBM Cloud) (#23299)
* Adding Watson TTS (IBM Cloud)

* Code review changes
2019-05-18 23:05:59 +02:00
Joakim Sørensen
57bd4185d4 Fixes issue with multiple alerts (#23945)
* Fixes issue with multiple alerts

* Adds missing new line

* Remove whitespace
2019-05-18 22:59:33 +02:00
Martin Donlon
91ba35c68e Update russound_rio dependency to version 0.1.7 (#23973)
v0.1.7 fixes async import issues in python 3.7+
2019-05-18 22:56:34 +02:00
Cyro
a99e15343c Make Discord payload data key not required (#23964) 2019-05-18 13:14:53 -07:00
Daniel Høyer Iversen
4583638b92 upgrade broadlink library (#23966) 2019-05-18 13:14:11 -07:00
Matt Snyder
10a1b156e3 Doorbird Refactor (#23892)
* Remove schedule management. Allow custom HTTP events defined in the configuration

* Consolidate doorbird request handling.  Make token a per device configuration item.

* Lint fixes

* Do not register dummy listener

* Remove punctuation
2019-05-18 21:46:00 +02:00
Daniel Høyer Iversen
a8286535eb Upate xiaomi voltage parser, fix #23898 (#23962) 2019-05-18 11:01:30 +02:00
Adrian Schröter
05146badf1 show battery level also when vacuum has no map support (#23947) 2019-05-18 10:27:05 +02:00
Josef Schlehofer
c483e4479e Update requests to 2.22.0 (#23958) 2019-05-17 20:41:22 -05:00
Julien Brochet
4a70c725b4 Setup integration dependencies before loading it (#23957) 2019-05-17 19:17:26 -05:00
SiliconAvatar
33ed017851 Add unit of measurement to Tautulli sensor (#23873)
Adds unit of measurement ("Watching") to sensor, so it can be graphed properly.
This is the same unit of measurement as the Plex sensor.
2019-05-17 23:02:56 +02:00
Joakim Sørensen
fffc4dd3fd Bump pytraccar (#23939) 2019-05-17 09:56:04 -04:00
bouni
e072981295 Added support for sensor other than temperature and humidity (#23863)
* Added support for sensor other than temperature and humidity

* fixed lint errors

* fixed minor issues pointed out by @fabaff
2019-05-17 09:47:10 +02:00
Pascal Vizeli
5d983d0b61 Fix auto version update Hass.io (#23935) 2019-05-17 09:39:36 +02:00
Fredrik Erlandsson
727f667cbc Fix fan rates for Daikin (#23860) 2019-05-17 09:36:47 +02:00
Fredrik Erlandsson
1b4fc2ae8d Fix for non existing Daikin zones (#23792) 2019-05-17 09:25:07 +02:00
Jc2k
5b0d1415ad Have homekit_controller use device registry (#23874)
* Add device registry support

* HK doesn't use mac as a connection id
2019-05-17 08:41:20 +02:00
Paulus Schoutsen
7818c98c67 Merge pull request #23932 from home-assistant/rc
0.93.1
2019-05-17 08:03:45 +02:00
Paulus Schoutsen
e12222697c Bumped version to 0.93.1 2019-05-17 06:35:20 +02:00
karlkar
58f28f177d Fix problem with cameras that don't support time (#23924)
Some onvif cameras don't support Date management. In that case None is returned and script crashes when trying to obtain date
2019-05-17 06:35:09 +02:00
Joakim Plate
6030e419c5 Switch media player to SWITCH type (#23914)
MEDIA device type is being rejected by google now.
2019-05-17 06:33:20 +02:00
Paulus Schoutsen
8d2a784831 Update Honeywell warning (#23913) 2019-05-17 06:33:20 +02:00
Pascal Vizeli
5dc841ecae Fix Hassio-version for Azure Pipelines (#23895) 2019-05-17 06:33:19 +02:00
karlkar
edf34eea94 Fix problem with cameras that don't support time (#23924)
Some onvif cameras don't support Date management. In that case None is returned and script crashes when trying to obtain date
2019-05-17 06:29:52 +02:00
Paulus Schoutsen
a303f67d3b Merge branch 'master' into dev 2019-05-17 06:28:36 +02:00
Aaron Bach
1b5f526e09 Fix additional IQVIA data bug (#23931) 2019-05-16 18:30:09 -06:00
Jc2k
03a0a3572b Fix icons for homekit_controller sensors (#23921) 2019-05-16 22:30:48 +01:00
Aaron Bach
297d24c5b0 Fix bug when IQVIA API fails to return data (#23916)
* Fix bug when IQVIA API fails to return data

* Updated requirements

* Fixed tests

* Linting

* Removed impossible case

* Removed extraneous comment
2019-05-16 15:19:53 -06:00
Joakim Plate
c8cf06b8b7 Switch media player to SWITCH type (#23914)
MEDIA device type is being rejected by google now.
2019-05-16 22:34:40 +02:00
David McNett
49d6d7c656 Version bump insteonplm to 0.15.4 (#23918)
* Version bump insteonplm to 0.15.4

* Package-level version change
2019-05-16 22:34:06 +02:00
Robbie Trencheny
96fd874090 Add @Kane610 to zeroconf CODEOWNERS 2019-05-16 12:28:24 -07:00
Paulus Schoutsen
c9703872e2 Update Honeywell warning (#23913) 2019-05-16 13:21:38 -06:00
Robbie Trencheny
2f5d7d4522 [WIP] Simplify zeroconf (#23890)
* Simplify zeroconf

* Remove unused imports
2019-05-16 12:04:20 -07:00
Daniel Høyer Iversen
7716e8fb68 Netatmo, handle offline device (#23907)
* Netatmo, handle offline device

* style
2019-05-16 18:07:37 +02:00
Erik Montnemery
c2fc8a0d61 Load HA core config from storage (#23872)
* Load HA core config from storage

* Tweak

* Lint, review comments

* Fix test

* Add tests

* Lint

* Address comments
2019-05-16 16:27:53 +02:00
Markus Jankowski
9be384690a Enable Homematic IP cloud climate device with HeatingThermostat only (#23776)
* Enable climate device with HeatingThermostat only

* Fix after review
2019-05-16 15:10:30 +02:00
Jc2k
692eeb3687 Fix ecobee 3 homekit pairing (#23882) 2019-05-16 14:32:13 +02:00
Pascal Vizeli
213c91ae73 Update azure-pipelines.yml for Azure Pipelines 2019-05-16 09:28:08 +02:00
Pascal Vizeli
36b1a89f93 Fix Hassio-version for Azure Pipelines (#23895) 2019-05-16 08:57:43 +02:00
Paulus Schoutsen
584bfbaa76 Merge pull request #23864 from home-assistant/rc
0.93.0
2019-05-16 07:08:27 +02:00
Paulus Schoutsen
0f140751b2 Fix PS4 blocking startup (#23893) 2019-05-16 05:43:45 +02:00
Paulus Schoutsen
6b359c95da Fix PS4 blocking startup (#23893) 2019-05-16 05:43:19 +02:00
starkillerOG
1fec64a1b3 Update Pynetgear to v0.6.1 (#23886)
* Update Pynetgear to v0.6.1

* update pynetgear to v0.6.1
2019-05-15 23:53:02 +02:00
Paulus Schoutsen
70ed58a78d Restructure device tracker (#23862)
* Restructure device tracker

* Docstyle

* Fix typing

* Lint

* Lint

* Fix tests
2019-05-15 23:43:45 +02:00
Pascal Vizeli
6aa9844f8f Fix auto discovery if the monitor condition (#23880) 2019-05-15 16:28:35 +02:00
Pascal Vizeli
7a4238095d Fix auto discovery if the monitor condition (#23880) 2019-05-15 16:27:41 +02:00
Paulus Schoutsen
177594f02c Update sensor.py 2019-05-15 14:00:42 +02:00
Ville Skyttä
cf89f45697 Fix homekit test assert no messages (#23856) 2019-05-15 13:13:56 +02:00
Penny Wood
2dc78e6f0c Take code owner for sun.sun (#23877)
* Take code owner

* Post hassfest
2019-05-15 03:14:35 -05:00
Penny Wood
9da74dda43 Quiet the chatty sun.sun (#23832)
* Split up method to allow caching event

* Lower frequency of updates.

* Code review patches.

* Minor changes to test

* Skip end of period at fixed multiple of delta.
Improved documentation.
2019-05-15 15:02:29 +08:00
David Bonnes
18149dcb8c Add geniushub sensor and binary_sensor (#23811)
* Initial commit

* add lastComms and de-lint

* dummy commit

* dummy commit 2

* refactor to temp in favour of battery

* back to battery, and no temp

* use snake_case

* Bump client

* only v3 API exposes device attributes

* delint

* delint2

* Change GeniusSwitch to GensiusBinarySensor
2019-05-14 23:30:26 +02:00
Pascal Vizeli
3f841a36a5 Update azure-pipelines.yml for Azure Pipelines
Automated version updates
2019-05-14 22:59:12 +02:00
damarco
80ae02cc49 Fix zha timed off (#23849) 2019-05-14 13:41:27 +02:00
Paulus Schoutsen
421b2962c6 Bumped version to 0.93.0 2019-05-14 13:18:36 +02:00
Paulus Schoutsen
bde5a9ef01 Bumped version to 0.93.0b4 2019-05-14 13:12:30 +02:00
Robbie Trencheny
b79886ad85 Fix improper usage of body attribute on web.Response. Should be text since we arent sending bytes (#23857) 2019-05-14 13:12:24 +02:00
Paulus Schoutsen
94a2fd542e Fix aiohttp response serialize (#23858)
* Fix aiohttp response serialize

* Suport bytes

* Handle None
2019-05-14 11:59:27 +02:00
Paulus Schoutsen
6fa8556033 Use Cloudhooks for OwnTracks (#23847)
* Use Cloudhooks for OwnTracks

* Update config_flow.py

* Update config_flow.py
2019-05-14 11:59:11 +02:00
Pascal Vizeli
19cfa8cf22 Update azure-pipelines.yml for Azure Pipelines
Automated version updates
2019-05-14 10:18:01 +02:00
Paulus Schoutsen
a859997190 Allow deletion of automations and scripts (#23845) 2019-05-14 09:16:36 +02:00
Robbie Trencheny
6f9860b25e Fix improper usage of body attribute on web.Response. Should be text since we arent sending bytes (#23857) 2019-05-14 09:12:05 +02:00
David F. Mulcahey
128ce589e1 Correct ZHA illumination conversion (#23853)
* fix illumination values

* correct formula

* update illuminance calculation

* update test
2019-05-14 07:16:41 +02:00
David F. Mulcahey
9b21774392 Fix ZHA battery when readings produce an unknown value (#23854)
* check for unknown readings

* only publish valid readings

* remove unused constant
2019-05-14 07:16:21 +02:00
David F. Mulcahey
eaf4a75402 bump zha-quirks (#23855) 2019-05-14 07:15:31 +02:00
Paulus Schoutsen
a1a6d4a631 Updated frontend to 20190514.0 2019-05-14 07:14:40 +02:00
Paulus Schoutsen
de1fd5a7fa WS: Improve service calling errors (#23840)
* WS: Improve service calling errors

* Docstyle

* Types

* Update text
2019-05-14 07:09:11 +02:00
Robert Svensson
0d96095646 Zeroconf - replace library (#23835)
* Use aiozeroconf in preparation for new zeroconf discovery

* Update requirements

* Remove sleep

* Make stop zeroconf a coroutine

* Remove unused import

* Fix aiozeroconf dependency in default_config tests
2019-05-14 05:58:13 +02:00
Paulus Schoutsen
45085dd97f Better handle large amounts of data being sent over WS (#23842)
* Better handle large amounts of data being sent over WS

* Lint
2019-05-14 05:57:47 +02:00
sander76
b2a1204bc5 Fix for battery device: new_device referenced before assignment. (#23793)
* Fix for battery device: new_device referenced before assignment.

* Fix buttons and switches mixup

* Update __init__.py

* Update binary_sensor.py

* Update __init__.py

* Update __init__.py

* Update binary_sensor.py

* Update __init__.py

* Update binary_sensor.py

* typo and indentation fixes

* low_bat and lowbat to uppercase.
2019-05-13 20:52:55 +02:00
damarco
990a9e80a2 Fix zha timed off (#23849) 2019-05-13 13:13:57 -04:00
Fredrik Erlandsson
0ffcc197d4 Daikin adaptions for AirBase units (#23734)
* updated list of supported fan_modes

* AirBase units does not support Holiday-mode

* AirBase units does not support outside temp

* pydaikin version bump

* don't modify constant
2019-05-13 15:38:33 +02:00
Baptiste Candellier
1a051f038d Add new SmartHab light and cover platform (#21225)
* Add SmartHab platform

* Remove url config entry, improve error handling

* Upgrade smarthab dependency

* Address comments

* Lint
2019-05-13 03:35:31 -07:00
Paulus Schoutsen
1e22c8daca Automatically generate config flow list (#23802)
* Add config flow to manifest.json

* Still load config flows via config flow platform

* Fix typo

* Lint

* Update config_flows.py"

* Catch import error when setting up entry

* Lint

* Fix tests

* Fix imports

* Lint

* Fix Unifi tests

* Fix translation test

* Add homekit_controller config flow
2019-05-13 01:16:55 -07:00
Jc2k
b8cbd39985 HomeKit Controller: Adopt config entries for pairing with homekit accessories (#23825)
* Fix user initiated pairing + show more user friendly name

* Add lock around async_refresh_entity_map

* Migrate homekit_controller to config entries.

* Improve docstring

Co-Authored-By: Martin Hjelmare <marhje52@kth.se>

* Add dummy async_setup_platform

* add_service -> async_add_service

* Add missing returns

* Enable coverage checks for homekit_controller
2019-05-12 23:56:05 -07:00
Paulus Schoutsen
3508622e3b Remove badges from README [skipci] (#23815) 2019-05-12 23:55:16 -07:00
Paulus Schoutsen
b8f6d824fd Catch import error when processing config (#23833) 2019-05-12 23:54:55 -07:00
akloeckner
e687848152 Make broadlink switch restore its state (#23829)
* Make broadlink switch restore its state

Method copied from pilight switch

* style
2019-05-12 19:28:33 +02:00
David Bonnes
2a9fd9ae26 Add incomfort climate and bump client (#23830)
* Initial commit

* bump client for bugfix

* bump client for bugfix 2

* de-lint
2019-05-12 13:40:10 +02:00
Paulus Schoutsen
3ec4070d8c Fix patching right import (#23816) 2019-05-11 19:29:29 +02:00
mvn23
6f8038992c Bump pyotgw to 0.4b4, fix Opentherm Gateway name in manifest.json (#23810) 2019-05-11 16:15:35 +02:00
Fabian Affolter
5c9a58f3e6 Upgrade youtube_dl to 2019.05.11 (#23808) 2019-05-11 16:15:09 +02:00
Stephen Benjamin
d34214ad32 Bump venstarcolortouch to v0.7 (#23806) 2019-05-11 10:33:18 +02:00
Andre Lengwenus
2b7021407c Add LCN climate platform (#22542)
* Add LCN climate component

* Updates of ha_state are done async

* Changes due to manifest.json
2019-05-11 10:24:02 +02:00
Jason Hunter
03cd4480df fix onvif wsdl import - take 2 (#23807) 2019-05-10 23:15:21 -07:00
Alexei Chetroi
910825580e Do not add coordinator to the ZHA entities. (#23803) 2019-05-10 18:57:08 -04:00
Paulus Schoutsen
c8d479e594 Updated frontend to 20190510.0 2019-05-10 14:22:38 -07:00
Anders Melchiorsen
34f6245e74 Synchronize Sonos service calls (#23791) 2019-05-10 22:37:03 +02:00
Paulus Schoutsen
e9ea5c2ccb Move tests to right folder (#23790)
* Move tests to right folder

* Fix test leaving files behind
2019-05-10 13:20:50 -07:00
David Bonnes
4347a0f6b7 Centralize geniushub updates (#23764)
* add hub/parent/manager

* add hub/parent/manager 2

* working now

* delint

* add back water heater

* make water_heater a child

* make water_heater a child - delint

* make water_heater a child - delint 2

* improve turn_on logic, and small tidy-up

* improve turn_on logic 2

* improve turn_on logic 3 - better docstring

* improve turn_on logic 3 - better docstring

* remove unnecessary DICT.get()s

* remove unnecessary DICT.get()s 2

* code tidy-up

* de-lint

* refactor for GeniusData

* refactor for GeniusData 2

* code tidy-up

* add missing should_poll = False
2019-05-10 18:34:28 +02:00
Paulus Schoutsen
5888e32360 Add support for an external step in config flow (#23782)
* Add support for an external step in config flow

* Types

* Lint
2019-05-10 14:33:49 +02:00
Andrey Kupreychik
4214a354a7 Bumped keenetic NDMS2 client version (#23786) 2019-05-10 11:43:43 +02:00
Paulus Schoutsen
369afd7ddd Update sensor.py 2019-05-09 22:01:37 -07:00
dreed47
281445917b Fix for issue #23739. Added unique_id property so (#23769)
that entities will always get mapped to the same
property ZPID code.
2019-05-09 20:18:28 -07:00
Jason Hunter
df6846344d Beta Fix: ONVIF (#23787)
* bump package to include wsdl

* update requirements all
2019-05-09 20:17:55 -07:00
Steven Looman
05960fa29c Sort discovered entries by 'st' to ensure getting the same device each discovery (#23763) 2019-05-09 16:17:46 -07:00
Pawel
068749bcbe fix two times creating JWT headers. (#23777) 2019-05-09 16:13:13 -07:00
Paulus Schoutsen
f21f32778f Updated frontend to 20190509.0 2019-05-09 15:49:12 -07:00
sander76
8ef3c6d4d3 Add battery binary sensor to homematic (#23067)
* first proposal

* parameter rename

* retrigger CI

* remove separate binary sensor

* remove batter_sensor

* battery device distinction at binary sensor discovery
2019-05-09 10:38:51 -07:00
Joakim Plate
c7a78ed522 Add stepped volume to demo (#23759)
* Add stepped volume to demo

* Simplify somewhat to avoid extra check
2019-05-09 10:18:22 -07:00
Aaron Bach
45adb5c9c7 Add config entry for IQVIA (#23765)
* Add config entry for IQVIA

* Updated tests and requirements

* Removed unnecessary dependency

* Fixed tests

* Reverted unintended change
2019-05-09 09:11:51 -07:00
Ties de Kock
4004867eda Split up yaml loaders into multiple files (#23774)
* Start moving parts of yaml utils to own module

Move parts of yaml loader out of the single large file and start
to create the structure of the yaml loaders in Ansible [0].

[0]: https://github.com/ansible/ansible/tree/devel/lib/ansible/parsing/yaml

* Finish yaml migration, update tests and mocks

  * Move code around to finish the migration
  * Update the mocks so that `open` is patched in
    `homeassistant.util.yaml.loader` instead of
    `homeassistant.util.yaml`.
  * Updated mypy ignores
  * Updated external API of `homeasistant.util.yaml`, see below:

Checked what part of the api of `homeassistant.util.yaml` was actually
called from outside the tests and added an `__ALL__` that contains only
these elements.

Updated the tests so that references to internal parts of the API (e.g.
the yaml module imported into `homeassistant.util.yaml.loader`) are
referenced directly from `homeassistant.util.yaml.loader`.

In `tests/test_yaml.py` the import `yaml` refers to
`homeassistant.util.yaml` and `yaml_loader` refers to `~.loader`.

Future work that remains for the next iteration is to create a custom
SafeConstructor and refers to that instead of monkey patching `yaml` with
custom loaders.

* Update mocks in yaml dumper, check_config
2019-05-09 09:07:56 -07:00
Markus Jankowski
118d3bc11c Add Presence Detector Indoor to Homematic IP (#23755)
* Add presence detector indoor

use device classes constants

* Add illuminance

* isort
2019-05-09 09:57:02 +02:00
cgtobi
0e9d71f232 Bump pyatmo to v1.11 (#23766) 2019-05-08 21:08:07 -07:00
Paulus Schoutsen
b552fbe312 Version bump to 0.94.0.dev0 2019-05-08 20:23:31 -07:00
647 changed files with 13127 additions and 5272 deletions

View File

@@ -47,6 +47,7 @@ omit =
homeassistant/components/august/*
homeassistant/components/automatic/device_tracker.py
homeassistant/components/avion/light.py
homeassistant/components/azure_event_hub/*
homeassistant/components/baidu/tts.py
homeassistant/components/bbb_gpio/*
homeassistant/components/bbox/device_tracker.py
@@ -171,6 +172,7 @@ omit =
homeassistant/components/esphome/camera.py
homeassistant/components/esphome/climate.py
homeassistant/components/esphome/cover.py
homeassistant/components/esphome/entry_data.py
homeassistant/components/esphome/fan.py
homeassistant/components/esphome/light.py
homeassistant/components/esphome/sensor.py
@@ -250,7 +252,6 @@ omit =
homeassistant/components/hitron_coda/device_tracker.py
homeassistant/components/hive/*
homeassistant/components/hlk_sw16/*
homeassistant/components/homekit_controller/*
homeassistant/components/homematic/*
homeassistant/components/homematic/climate.py
homeassistant/components/homematic/cover.py
@@ -344,6 +345,7 @@ omit =
homeassistant/components/mastodon/notify.py
homeassistant/components/matrix/*
homeassistant/components/maxcube/*
homeassistant/components/mcp23017/*
homeassistant/components/media_extractor/*
homeassistant/components/mediaroom/media_player.py
homeassistant/components/message_bird/notify.py
@@ -487,6 +489,9 @@ omit =
homeassistant/components/reddit/*
homeassistant/components/rejseplanen/sensor.py
homeassistant/components/remember_the_milk/__init__.py
homeassistant/components/repetier/__init__.py
homeassistant/components/repetier/sensor.py
homeassistant/components/remote_rpi_gpio/*
homeassistant/components/rest/binary_sensor.py
homeassistant/components/rest/notify.py
homeassistant/components/rest/switch.py
@@ -539,12 +544,14 @@ omit =
homeassistant/components/slack/notify.py
homeassistant/components/sma/sensor.py
homeassistant/components/smappee/*
homeassistant/components/smarthab/*
homeassistant/components/smtp/notify.py
homeassistant/components/snapcast/media_player.py
homeassistant/components/snmp/*
homeassistant/components/sochain/sensor.py
homeassistant/components/socialblade/sensor.py
homeassistant/components/solaredge/sensor.py
homeassistant/components/solax/sensor.py
homeassistant/components/somfy_mylink/*
homeassistant/components/sonarr/sensor.py
homeassistant/components/songpal/media_player.py
@@ -651,6 +658,7 @@ omit =
homeassistant/components/waqi/sensor.py
homeassistant/components/waterfurnace/*
homeassistant/components/watson_iot/*
homeassistant/components/watson_tts/tts.py
homeassistant/components/waze_travel_time/sensor.py
homeassistant/components/webostv/*
homeassistant/components/wemo/*

View File

@@ -32,6 +32,7 @@ homeassistant/components/automatic/* @armills
homeassistant/components/automation/* @home-assistant/core
homeassistant/components/aws/* @awarecan @robbiet480
homeassistant/components/axis/* @kane610
homeassistant/components/azure_event_hub/* @eavanvalkenburg
homeassistant/components/bitcoin/* @fabaff
homeassistant/components/bizkaibus/* @UgaitzEtxebarria
homeassistant/components/blink/* @fronzbot
@@ -83,7 +84,7 @@ homeassistant/components/flock/* @fabaff
homeassistant/components/flunearyou/* @bachya
homeassistant/components/foursquare/* @robbiet480
homeassistant/components/freebox/* @snoof85
homeassistant/components/frontend/* @home-assistant/core
homeassistant/components/frontend/* @home-assistant/frontend
homeassistant/components/gearbest/* @HerrHofrat
homeassistant/components/geniushub/* @zxdavb
homeassistant/components/gitter/* @fabaff
@@ -131,6 +132,7 @@ homeassistant/components/kodi/* @armills
homeassistant/components/konnected/* @heythisisnate
homeassistant/components/lametric/* @robbiet480
homeassistant/components/launch_library/* @ludeeus
homeassistant/components/lcn/* @alengwenus
homeassistant/components/lifx/* @amelchio
homeassistant/components/lifx_cloud/* @amelchio
homeassistant/components/lifx_legacy/* @amelchio
@@ -138,11 +140,12 @@ homeassistant/components/linux_battery/* @fabaff
homeassistant/components/liveboxplaytv/* @pschmitt
homeassistant/components/logger/* @home-assistant/core
homeassistant/components/logi_circle/* @evanjd
homeassistant/components/lovelace/* @home-assistant/core
homeassistant/components/lovelace/* @home-assistant/frontend
homeassistant/components/luci/* @fbradyirl
homeassistant/components/luftdaten/* @fabaff
homeassistant/components/mastodon/* @fabaff
homeassistant/components/matrix/* @tinloaf
homeassistant/components/mcp23017/* @jardiamj
homeassistant/components/mediaroom/* @dgomes
homeassistant/components/melissa/* @kennedyshead
homeassistant/components/met/* @danielhiversen
@@ -173,8 +176,8 @@ homeassistant/components/openuv/* @bachya
homeassistant/components/openweathermap/* @fabaff
homeassistant/components/orangepi_gpio/* @pascallj
homeassistant/components/owlet/* @oblogic7
homeassistant/components/panel_custom/* @home-assistant/core
homeassistant/components/panel_iframe/* @home-assistant/core
homeassistant/components/panel_custom/* @home-assistant/frontend
homeassistant/components/panel_iframe/* @home-assistant/frontend
homeassistant/components/persistent_notification/* @home-assistant/core
homeassistant/components/philips_js/* @elupus
homeassistant/components/pi_hole/* @fabaff
@@ -190,6 +193,7 @@ homeassistant/components/qwikswitch/* @kellerza
homeassistant/components/raincloud/* @vanstinator
homeassistant/components/rainmachine/* @bachya
homeassistant/components/random/* @fabaff
homeassistant/components/repetier/* @MTrab
homeassistant/components/rfxtrx/* @danielhiversen
homeassistant/components/rmvtransport/* @cgtobi
homeassistant/components/roomba/* @pschmitt
@@ -205,15 +209,17 @@ homeassistant/components/shiftr/* @fabaff
homeassistant/components/shodan/* @fabaff
homeassistant/components/simplisafe/* @bachya
homeassistant/components/sma/* @kellerza
homeassistant/components/smarthab/* @outadoc
homeassistant/components/smartthings/* @andrewsayre
homeassistant/components/smtp/* @fabaff
homeassistant/components/solax/* @squishykid
homeassistant/components/sonos/* @amelchio
homeassistant/components/spaceapi/* @fabaff
homeassistant/components/spider/* @peternijssen
homeassistant/components/sql/* @dgomes
homeassistant/components/statistics/* @fabaff
homeassistant/components/stiebel_eltron/* @fucm
homeassistant/components/sun/* @home-assistant/core
homeassistant/components/sun/* @Swamp-Ig
homeassistant/components/supla/* @mwegrzynek
homeassistant/components/swiss_hydrological_data/* @fabaff
homeassistant/components/swiss_public_transport/* @fabaff
@@ -253,6 +259,7 @@ homeassistant/components/velux/* @Julius2342
homeassistant/components/version/* @fabaff
homeassistant/components/vizio/* @raman325
homeassistant/components/waqi/* @andrey-git
homeassistant/components/watson_tts/* @rutkai
homeassistant/components/weather/* @fabaff
homeassistant/components/weblink/* @home-assistant/core
homeassistant/components/websocket_api/* @home-assistant/core
@@ -261,14 +268,14 @@ homeassistant/components/worldclock/* @fabaff
homeassistant/components/xfinity/* @cisasteelersfan
homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
homeassistant/components/xiaomi_miio/* @rytilahti @syssi
homeassistant/components/xiaomi_tv/* @fattdev
homeassistant/components/xiaomi_tv/* @simse
homeassistant/components/xmpp/* @fabaff @flowolf
homeassistant/components/yamaha_musiccast/* @jalmeroth
homeassistant/components/yeelight/* @rytilahti @zewelor
homeassistant/components/yeelightsunflower/* @lindsaymarkward
homeassistant/components/yessssms/* @flowolf
homeassistant/components/yi/* @bachya
homeassistant/components/zeroconf/* @robbiet480
homeassistant/components/zeroconf/* @robbiet480 @Kane610
homeassistant/components/zha/* @dmulcahey @adminiuga
homeassistant/components/zone/* @home-assistant/core
homeassistant/components/zoneminder/* @rohankapoorcom

View File

@@ -1,4 +1,4 @@
Home Assistant |Build Status| |CI Status| |Coverage Status| |Chat Status|
Home Assistant |Chat Status|
=================================================================================
Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control.
@@ -27,12 +27,6 @@ components <https://developers.home-assistant.io/docs/en/creating_component_inde
If you run into issues while using Home Assistant or during development
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
.. |Build Status| image:: https://travis-ci.org/home-assistant/home-assistant.svg?branch=dev
:target: https://travis-ci.org/home-assistant/home-assistant
.. |CI Status| image:: https://circleci.com/gh/home-assistant/home-assistant.svg?style=shield
:target: https://circleci.com/gh/home-assistant/home-assistant
.. |Coverage Status| image:: https://img.shields.io/coveralls/home-assistant/home-assistant.svg
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
:target: https://discord.gg/c5DvZ4e
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png

168
azure-pipelines-release.yml Normal file
View File

@@ -0,0 +1,168 @@
# https://dev.azure.com/home-assistant
trigger:
batch: true
tags:
include:
- '*'
pr: none
variables:
- name: versionBuilder
value: '3.2'
- group: docker
- group: github
- group: twine
jobs:
- job: 'VersionValidate'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
displayName: 'Use Python 3.7'
inputs:
versionSpec: '3.7'
- script: |
setup_version="$(python setup.py -V)"
branch_version="$(Build.SourceBranchName)"
if [ "${setup_version}" != "${branch_version}" ]; then
echo "Version of tag ${branch_version} don't match with ${setup_version}!"
exit 1
fi
displayName: 'Check version of branch/tag'
- script: |
sudo apt-get install -y --no-install-recommends \
jq curl
release="$(Build.SourceBranchName)"
created_by="$(curl -s https://api.github.com/repos/home-assistant/home-assistant/releases/tags/${release} | jq --raw-output '.author.login')"
if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480)$ ]]; then
exit 0
fi
echo "${created_by} is not allowed to create an release!"
exit 1
displayName: 'Check rights'
- job: 'ReleasePython'
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate'))
dependsOn:
- 'VersionValidate'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
displayName: 'Use Python 3.7'
inputs:
versionSpec: '3.7'
- script: pip install twine wheel
displayName: 'Install tools'
- script: python setup.py sdist bdist_wheel
displayName: 'Build package'
- script: |
export TWINE_USERNAME="$(twineUser)"
export TWINE_PASSWORD="$(twinePassword)"
twine upload dist/* --skip-existing
displayName: 'Upload pypi'
- job: 'ReleaseDocker'
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate'))
dependsOn:
- 'VersionValidate'
timeoutInMinutes: 240
pool:
vmImage: 'ubuntu-latest'
strategy:
maxParallel: 5
matrix:
amd64:
buildArch: 'amd64'
buildMachine: 'qemux86-64,intel-nuc'
i386:
buildArch: 'i386'
buildMachine: 'qemux86'
armhf:
buildArch: 'armhf'
buildMachine: 'qemuarm,raspberrypi'
armv7:
buildArch: 'armv7'
buildMachine: 'raspberrypi2,raspberrypi3,odroid-xu,tinker'
aarch64:
buildArch: 'aarch64'
buildMachine: 'qemuarm-64,raspberrypi3-64,odroid-c2,orangepi-prime'
steps:
- script: sudo docker login -u $(dockerUser) -p $(dockerPassword)
displayName: 'Docker hub login'
- script: sudo docker pull homeassistant/amd64-builder:$(versionBuilder)
displayName: 'Install Builder'
- script: |
set -e
sudo docker run --rm --privileged \
-v ~/.docker:/root/.docker \
-v /run/docker.sock:/run/docker.sock:rw \
homeassistant/amd64-builder:$(versionBuilder) \
--homeassistant $(Build.SourceBranchName) "--$(buildArch)" \
-r https://github.com/home-assistant/hassio-homeassistant \
-t generic --docker-hub homeassistant
sudo docker run --rm --privileged \
-v ~/.docker:/root/.docker \
-v /run/docker.sock:/run/docker.sock:rw \
homeassistant/amd64-builder:$(versionBuilder) \
--homeassistant-machine "$(Build.SourceBranchName)=$(buildMachine)" \
-r https://github.com/home-assistant/hassio-homeassistant \
-t machine --docker-hub homeassistant
displayName: 'Build Release'
- job: 'ReleaseHassio'
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('ReleaseDocker'))
dependsOn:
- 'ReleaseDocker'
pool:
vmImage: 'ubuntu-latest'
steps:
- script: |
sudo apt-get install -y --no-install-recommends \
git jq curl
git config --global user.name "Pascal Vizeli"
git config --global user.email "pvizeli@syshack.ch"
git config --global credential.helper store
echo "https://$(githubToken):x-oauth-basic@github.com" > $HOME/.git-credentials
displayName: 'Install requirements'
- script: |
set -e
version="$(Build.SourceBranchName)"
git clone https://github.com/home-assistant/hassio-version
cd hassio-version
dev_version="$(jq --raw-output '.homeassistant.default' dev.json)"
beta_version="$(jq --raw-output '.homeassistant.default' beta.json)"
stable_version="$(jq --raw-output '.homeassistant.default' stable.json)"
if [[ "$version" =~ b ]]; then
sed -i "s|$dev_version|$version|g" dev.json
sed -i "s|$beta_version|$version|g" beta.json
else
sed -i "s|$dev_version|$version|g" dev.json
sed -i "s|$beta_version|$version|g" beta.json
sed -i "s|$stable_version|$version|g" stable.json
fi
git commit -am "Bump Home Assistant $version"
git push
displayName: 'Update version files'

View File

@@ -1,143 +0,0 @@
# https://dev.azure.com/home-assistant
trigger:
batch: true
branches:
include:
- dev
tags:
include:
- '*'
variables:
- name: versionBuilder
value: '3.2'
- name: versionWheels
value: '0.3'
- group: docker
- group: wheels
jobs:
- job: 'Wheels'
condition: eq(variables['Build.SourceBranchName'], 'dev')
timeoutInMinutes: 360
pool:
vmImage: 'ubuntu-16.04'
strategy:
maxParallel: 3
matrix:
amd64:
buildArch: 'amd64'
i386:
buildArch: 'i386'
armhf:
buildArch: 'armhf'
armv7:
buildArch: 'armv7'
aarch64:
buildArch: 'aarch64'
steps:
- script: |
sudo apt-get install -y --no-install-recommends \
qemu-user-static \
binfmt-support
sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
sudo update-binfmts --enable qemu-arm
sudo update-binfmts --enable qemu-aarch64
displayName: 'Initial cross build'
- script: |
mkdir -p .ssh
echo -e "-----BEGIN RSA PRIVATE KEY-----\n$(wheelsSSH)\n-----END RSA PRIVATE KEY-----" >> .ssh/id_rsa
ssh-keyscan -H $(wheelsHost) >> .ssh/known_hosts
chmod 600 .ssh/*
displayName: 'Install ssh key'
- script: sudo docker pull homeassistant/$(buildArch)-wheels:$(versionWheels)
displayName: 'Install wheels builder'
- script: |
cp requirements_all.txt requirements_hassio.txt
# Enable because we can build it
sed -i "s|# pytradfri|pytradfri|g" requirements_hassio.txt
sed -i "s|# pybluez|pybluez|g" requirements_hassio.txt
sed -i "s|# bluepy|bluepy|g" requirements_hassio.txt
sed -i "s|# beacontools|beacontools|g" requirements_hassio.txt
sed -i "s|# RPi.GPIO|RPi.GPIO|g" requirements_hassio.txt
sed -i "s|# raspihats|raspihats|g" requirements_hassio.txt
sed -i "s|# rpi-rf|rpi-rf|g" requirements_hassio.txt
sed -i "s|# blinkt|blinkt|g" requirements_hassio.txt
sed -i "s|# fritzconnection|fritzconnection|g" requirements_hassio.txt
sed -i "s|# pyuserinput|pyuserinput|g" requirements_hassio.txt
sed -i "s|# evdev|evdev|g" requirements_hassio.txt
sed -i "s|# smbus-cffi|smbus-cffi|g" requirements_hassio.txt
sed -i "s|# i2csense|i2csense|g" requirements_hassio.txt
sed -i "s|# python-eq3bt|python-eq3bt|g" requirements_hassio.txt
sed -i "s|# pycups|pycups|g" requirements_hassio.txt
sed -i "s|# homekit|homekit|g" requirements_hassio.txt
sed -i "s|# decora_wifi|decora_wifi|g" requirements_hassio.txt
sed -i "s|# decora|decora|g" requirements_hassio.txt
sed -i "s|# PySwitchbot|PySwitchbot|g" requirements_hassio.txt
sed -i "s|# pySwitchmate|pySwitchmate|g" requirements_hassio.txt
# Disable because of error
sed -i "s|insteonplm|# insteonplm|g" requirements_hassio.txt
displayName: 'Prepare requirements files for Hass.io'
- script: |
sudo docker run --rm -v $(pwd):/data:ro -v $(pwd)/.ssh:/root/.ssh:rw \
homeassistant/$(buildArch)-wheels:$(versionWheels) \
--apk "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;linux-headers;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev" \
--index https://wheels.hass.io \
--requirement requirements_hassio.txt \
--upload rsync \
--remote wheels@$(wheelsHost):/opt/wheels
displayName: 'Run wheels build'
- job: 'Release'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
timeoutInMinutes: 120
pool:
vmImage: 'ubuntu-16.04'
strategy:
maxParallel: 5
matrix:
amd64:
buildArch: 'amd64'
buildMachine: 'qemux86-64,intel-nuc'
i386:
buildArch: 'i386'
buildMachine: 'qemux86'
armhf:
buildArch: 'armhf'
buildMachine: 'qemuarm,raspberrypi'
armv7:
buildArch: 'armv7'
buildMachine: 'raspberrypi2,raspberrypi3,odroid-xu,tinker'
aarch64:
buildArch: 'aarch64'
buildMachine: 'qemuarm-64,raspberrypi3-64,odroid-c2,orangepi-prime'
steps:
- script: sudo docker login -u $(dockerUser) -p $(dockerPassword)
displayName: 'Docker hub login'
- script: sudo docker pull homeassistant/amd64-builder:$(versionBuilder)
displayName: 'Install Builder'
- script: |
set -e
sudo docker run --rm --privileged \
-v ~/.docker:/root/.docker \
-v /run/docker.sock:/run/docker.sock:rw \
homeassistant/amd64-builder:$(versionBuilder) \
--homeassistant $(Build.SourceBranchName) "--$(buildArch)" \
-r https://github.com/home-assistant/hassio-homeassistant \
-t generic --docker-hub homeassistant
sudo docker run --rm --privileged \
-v ~/.docker:/root/.docker \
-v /run/docker.sock:/run/docker.sock:rw \
homeassistant/amd64-builder:$(versionBuilder) \
--homeassistant-machine "$(Build.SourceBranchName)=$(buildMachine)" \
-r https://github.com/home-assistant/hassio-homeassistant \
-t machine --docker-hub homeassistant
displayName: 'Build Release'

View File

@@ -94,6 +94,13 @@ async def async_from_config_dict(config: Dict[str, Any],
stop = time()
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
if sys.version_info[:3] < (3, 6, 0):
hass.components.persistent_notification.async_create(
"Python 3.5 support is deprecated and will "
"be removed in the first release after August 1. Please "
"upgrade Python.", "Python version", "python_version"
)
# TEMP: warn users for invalid slugs
# Remove after 0.94 or 1.0
if cv.INVALID_SLUGS_FOUND or cv.INVALID_ENTITY_IDS_FOUND:

View File

@@ -1,7 +1,7 @@
"""Support for repeating alerts when conditions are met."""
import asyncio
import logging
from datetime import datetime, timedelta
from datetime import timedelta
import voluptuous as vol
@@ -13,6 +13,7 @@ from homeassistant.const import (
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
from homeassistant.helpers import service, event
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.util.dt import now
_LOGGER = logging.getLogger(__name__)
@@ -117,7 +118,7 @@ async def async_setup(hass, config):
tasks = [alert.async_update_ha_state() for alert in entities]
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks)
return True
@@ -222,7 +223,7 @@ class Alert(ToggleEntity):
async def _schedule_notify(self):
"""Schedule a notification."""
delay = self._delay[self._next_delay]
next_msg = datetime.now() + delay
next_msg = now() + delay
self._cancel = \
event.async_track_point_in_time(self.hass, self._notify, next_msg)
self._next_delay = min(self._next_delay + 1, len(self._delay) - 1)

View File

@@ -39,7 +39,7 @@ class Auth:
self._prefs = None
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
self._get_token_lock = asyncio.Lock(loop=hass.loop)
self._get_token_lock = asyncio.Lock()
async def async_do_auth(self, accept_grant_code):
"""Do authentication with an AcceptGrant code."""
@@ -97,7 +97,7 @@ class Auth:
try:
session = aiohttp_client.async_get_clientsession(self.hass)
with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self.hass.loop):
with async_timeout.timeout(DEFAULT_TIMEOUT):
response = await session.post(LWA_TOKEN_URI,
headers=LWA_HEADERS,
data=lwa_params,

View File

@@ -1432,7 +1432,7 @@ async def async_send_changereport_message(hass, config, alexa_entity):
try:
session = aiohttp_client.async_get_clientsession(hass)
with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(DEFAULT_TIMEOUT):
response = await session.post(config.endpoint,
headers=headers,
json=message_serialized,

View File

@@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Error desconocido al generar un token de acceso.",
"already_setup": "La cuenta de Ambiclimate est\u00e1 configurada.",
"no_config": "Es necesario configurar Ambiclimate antes de poder autenticarse con \u00e9l. [Por favor, lee las instrucciones](https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Autenticado correctamente con Ambiclimate"
},
"error": {
"follow_link": "Accede al enlace e identif\u00edcate antes de pulsar Enviar.",
"no_token": "No autenticado con Ambiclimate"
},
"step": {
"auth": {
"description": "Accede al siguiente [enlace]({authorization_url}) y <b>permite</b> el acceso a tu cuenta de Ambiclimate, despu\u00e9s vuelve y pulsa en <b>enviar</b> a continuaci\u00f3n.\n(Aseg\u00farate que la url de devoluci\u00f3n de llamada es {cb_url})",
"title": "Autenticaci\u00f3n de Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'un jeton d'acc\u00e8s.",
"already_setup": "Le compte Ambiclimate est configur\u00e9.",
"no_config": "Vous devez configurer Ambiclimate avant de pouvoir vous authentifier aupr\u00e8s de celui-ci. [Veuillez lire les instructions] (https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Authentifi\u00e9 avec succ\u00e8s avec Ambiclimate"
},
"error": {
"follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.",
"no_token": "Non authentifi\u00e9 avec Ambiclimate"
},
"step": {
"auth": {
"description": "Suivez ce [lien] ( {authorization_url} ) et <b> Autorisez </b> l'acc\u00e8s \u00e0 votre compte Ambiclimate, puis revenez et appuyez sur <b> Envoyer </b> ci-dessous. \n (Assurez-vous que l'URL de rappel sp\u00e9cifi\u00e9 est {cb_url} )",
"title": "Authentifier Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Ukjent feil ved oppretting av tilgangstoken.",
"already_setup": "Ambiclimate-kontoen er konfigurert.",
"no_config": "Du m\u00e5 konfigurere Ambiclimate f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Vellykket autentisering med Ambiclimate"
},
"error": {
"follow_link": "Vennligst f\u00f8lg lenken og godkjen f\u00f8r du trykker p\u00e5 Send",
"no_token": "Ikke autentisert med Ambiclimate"
},
"step": {
"auth": {
"description": "Vennligst f\u00f8lg denne [linken]({authorization_url}) og <b>Tillat</b> tilgang til din Ambiclimate konto, og kom s\u00e5 tilbake og trykk <b>Send</b> nedenfor.\n(Kontroller at den angitte URL-adressen for tilbakeringing er {cb_url})",
"title": "Autensiere Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Nieznany b\u0142\u0105d podczas generowania tokena dost\u0119pu.",
"already_setup": "Konto Ambiclimate jest skonfigurowane.",
"no_config": "Musisz skonfigurowa\u0107 Ambiclimate, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Pomy\u015blnie uwierzytelniono z Ambiclimate"
},
"error": {
"follow_link": "Prosz\u0119 klikn\u0105\u0107 link i uwierzytelni\u0107 przed naci\u015bni\u0119ciem przycisku Prze\u015blij",
"no_token": "Nie uwierzytelniony z Ambiclimate"
},
"step": {
"auth": {
"description": "Kliknij poni\u017cszy [link]({authorization_url}) i <b>Zezw\u00f3l</b> na dost\u0119p do swojego konta Ambiclimate, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij <b>Prze\u015blij</b> poni\u017cej. \n(Upewnij si\u0119, \u017ce podany adres URL to {cb_url})",
"title": "Uwierzytelnienie Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Neznana napaka pri ustvarjanju \u017eetona za dostop.",
"already_setup": "Ra\u010dun Ambiclimate je konfiguriran.",
"no_config": "Ambiclimat morate konfigurirati, preden lahko z njo preverjate pristnost. [Preberite navodila] (https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Uspe\u0161no overjeno z funkcijo Ambiclimate"
},
"error": {
"follow_link": "Preden pritisnete Po\u0161lji, sledite povezavi in preverite pristnost",
"no_token": "Ni overjeno z Ambiclimate"
},
"step": {
"auth": {
"description": "Sledite temu povezavi ( {authorization_url} in <b> Dovoli </b> dostopu do svojega ra\u010duna Ambiclimate, nato se vrnite in pritisnite <b> Po\u0161lji </b> spodaj. \n (Poskrbite, da je dolo\u010den url za povratni klic {cb_url} )",
"title": "Overi Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Ok\u00e4nt fel vid generering av \u00e5tkomsttoken.",
"already_setup": "Ambiclientkontot \u00e4r konfigurerat",
"no_config": "Du m\u00e5ste konfigurera Ambiclimate innan du kan autentisera med den. [V\u00e4nligen l\u00e4s instruktionerna] (https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Lyckad autentisering med Ambiclimate"
},
"error": {
"follow_link": "V\u00e4nligen f\u00f6lj l\u00e4nken och autentisera dig innan du trycker p\u00e5 Skicka",
"no_token": "Inte autentiserad med Ambiclimate"
},
"step": {
"auth": {
"description": "V\u00e4nligen f\u00f6lj denna [l\u00e4nk] ({authorization_url}) och <b> till\u00e5ta </b> till g\u00e5ng till ditt Ambiclimate konto, kom sedan tillbaka och tryck p\u00e5 <b> Skicka </b> nedan.\n(Kontrollera att den angivna callback url \u00e4r {cb_url})",
"title": "Autentisera Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@@ -62,7 +62,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
return
if _token_info:
await store.async_save(token_info)
await store.async_save(_token_info)
token_info = _token_info
data_connection = ambiclimate.AmbiclimateConnection(oauth,

View File

@@ -1,9 +1,10 @@
{
"domain": "ambiclimate",
"name": "Ambiclimate",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/ambiclimate",
"requirements": [
"ambiclimate==0.1.1"
"ambiclimate==0.1.2"
],
"dependencies": [],
"codeowners": [

View File

@@ -1,6 +1,7 @@
{
"domain": "ambient_station",
"name": "Ambient station",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/ambient_station",
"requirements": [
"aioambient==0.3.0"

View File

@@ -203,8 +203,7 @@ class AmcrestCam(Camera):
"""Return the camera model."""
return self._model
@property
def stream_source(self):
async def stream_source(self):
"""Return the source of the stream."""
return self._api.rtsp_url(typeno=self._resolution)

View File

@@ -233,7 +233,7 @@ async def async_setup(hass, config):
tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks)
return True

View File

@@ -90,20 +90,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if CONF_ADB_SERVER_IP not in config:
# Use "python-adb" (Python ADB implementation)
adb_log = "using 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])
adb_log += "with adbkey='{0}'".format(config[CONF_ADBKEY])
else:
aftv = setup(host, device_class=config[CONF_DEVICE_CLASS])
adb_log = ""
adb_log += "without adbkey authentication"
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(
adb_log = "using ADB server at {0}:{1}".format(
config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT])
if not aftv.available:
@@ -117,7 +118,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
else:
device_name = 'Android TV / Fire TV device'
_LOGGER.warning("Could not connect to %s at %s%s",
_LOGGER.warning("Could not connect to %s at %s %s",
device_name, host, adb_log)
raise PlatformNotReady
@@ -156,10 +157,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for target_device in target_devices:
output = target_device.adb_command(cmd)
# log the output if there is any
if output and (not isinstance(output, str) or output.strip()):
# 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))
cmd, target_device.entity_id, output)
hass.services.register(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND,
service_adb_command,
@@ -224,6 +225,7 @@ class ADBDevice(MediaPlayerDevice):
self.exceptions = (ConnectionResetError, RuntimeError)
# Property attributes
self._adb_response = None
self._available = self.aftv.available
self._current_app = None
self._state = None
@@ -243,6 +245,11 @@ class ADBDevice(MediaPlayerDevice):
"""Return whether or not the ADB connection is valid."""
return self._available
@property
def device_state_attributes(self):
"""Provide the last ADB command's response as an attribute."""
return {'adb_response': self._adb_response}
@property
def name(self):
"""Return the device name."""
@@ -304,12 +311,24 @@ class ADBDevice(MediaPlayerDevice):
"""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))
self.aftv.adb_shell('input keyevent {}'.format(key))
self._adb_response = None
self.schedule_update_ha_state()
return
if cmd == 'GET_PROPERTIES':
return self.aftv.get_properties_dict()
self._adb_response = str(self.aftv.get_properties_dict())
self.schedule_update_ha_state()
return self._adb_response
return self.aftv.adb_shell(cmd)
response = self.aftv.adb_shell(cmd)
if isinstance(response, str) and response.strip():
self._adb_response = response.strip()
else:
self._adb_response = None
self.schedule_update_ha_state()
return self._adb_response
class AndroidTVDevice(ADBDevice):

View File

@@ -47,7 +47,7 @@ async def async_setup_platform(hass, config, async_add_entities,
hass.async_create_task(device.async_update_ha_state())
avr = await anthemav.Connection.create(
host=host, port=port, loop=hass.loop,
host=host, port=port,
update_callback=async_anthemav_update_callback)
device = AnthemAVR(avr, name)

View File

@@ -82,7 +82,7 @@ class APIEventStream(HomeAssistantView):
raise Unauthorized()
hass = request.app['hass']
stop_obj = object()
to_write = asyncio.Queue(loop=hass.loop)
to_write = asyncio.Queue()
restrict = request.query.get('restrict')
if restrict:
@@ -119,8 +119,7 @@ class APIEventStream(HomeAssistantView):
while True:
try:
with async_timeout.timeout(STREAM_PING_INTERVAL,
loop=hass.loop):
with async_timeout.timeout(STREAM_PING_INTERVAL):
payload = await to_write.get()
if payload is stop_obj:

View File

@@ -1,6 +1,5 @@
"""APNS Notification platform."""
import logging
import os
import voluptuous as vol
@@ -149,7 +148,8 @@ class ApnsNotificationService(BaseNotificationService):
self.devices = {}
self.device_states = {}
self.topic = topic
if os.path.isfile(self.yaml_path):
try:
self.devices = {
str(key): ApnsDevice(
str(key),
@@ -160,6 +160,8 @@ class ApnsNotificationService(BaseNotificationService):
for (key, value) in
load_yaml_config_file(self.yaml_path).items()
}
except FileNotFoundError:
pass
tracking_ids = [
device.full_tracking_device_id

View File

@@ -167,7 +167,7 @@ async def async_setup(hass, config):
tasks = [_setup_atv(hass, config, conf) for conf in config.get(DOMAIN, [])]
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks)
hass.services.async_register(
DOMAIN, SERVICE_SCAN, async_service_handler,

View File

@@ -124,7 +124,7 @@ async def async_setup(hass, config):
context=service_call.context))
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks)
async def turn_onoff_service_handler(service_call):
"""Handle automation turn on/off service calls."""
@@ -134,7 +134,7 @@ async def async_setup(hass, config):
tasks.append(getattr(entity, method)())
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks)
async def toggle_service_handler(service_call):
"""Handle automation toggle service calls."""
@@ -146,7 +146,7 @@ async def async_setup(hass, config):
tasks.append(entity.async_turn_on())
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks)
async def reload_service_handler(service_call):
"""Remove all automations and load new ones from config."""

View File

@@ -166,14 +166,14 @@ async def _validate_aws_credentials(hass, credential):
profile = aws_config.get(CONF_PROFILE_NAME)
if profile is not None:
session = aiobotocore.AioSession(profile=profile, loop=hass.loop)
session = aiobotocore.AioSession(profile=profile)
del aws_config[CONF_PROFILE_NAME]
if CONF_ACCESS_KEY_ID in aws_config:
del aws_config[CONF_ACCESS_KEY_ID]
if CONF_SECRET_ACCESS_KEY in aws_config:
del aws_config[CONF_SECRET_ACCESS_KEY]
else:
session = aiobotocore.AioSession(loop=hass.loop)
session = aiobotocore.AioSession()
if credential[CONF_VALIDATE]:
async with session.create_client("iam", **aws_config) as client:

View File

@@ -94,10 +94,10 @@ async def async_get_service(hass, config, discovery_info=None):
if session is None:
profile = aws_config.get(CONF_PROFILE_NAME)
if profile is not None:
session = aiobotocore.AioSession(profile=profile, loop=hass.loop)
session = aiobotocore.AioSession(profile=profile)
del aws_config[CONF_PROFILE_NAME]
else:
session = aiobotocore.AioSession(loop=hass.loop)
session = aiobotocore.AioSession()
aws_config[CONF_REGION] = region_name

View File

@@ -0,0 +1,18 @@
{
"config": {
"error": {
"device_unavailable": "Apparaat is niet beschikbaar",
"faulty_credentials": "Ongeldige gebruikersreferenties"
},
"step": {
"user": {
"data": {
"host": "Host",
"password": "Wachtwoord",
"port": "Poort",
"username": "Gebruikersnaam"
}
}
}
}
}

View File

@@ -2,7 +2,8 @@
"config": {
"abort": {
"already_configured": "Enheten \u00e4r redan konfigurerad",
"bad_config_file": "Felaktig data fr\u00e5n config fil"
"bad_config_file": "Felaktig data fr\u00e5n config fil",
"link_local_address": "Link local addresses are not supported"
},
"error": {
"already_configured": "Enheten \u00e4r redan konfigurerad",
@@ -17,7 +18,7 @@
"port": "Port",
"username": "Anv\u00e4ndarnamn"
},
"title": "Konfigurera Axis enhet"
"title": "Konfigurera Axis-enhet"
}
},
"title": "Axis enhet"

View File

@@ -0,0 +1,86 @@
"""Base classes for Axis entities."""
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from .const import DOMAIN as AXIS_DOMAIN
class AxisEntityBase(Entity):
"""Base common to all Axis entities."""
def __init__(self, device):
"""Initialize the Axis event."""
self.device = device
self.unsub_dispatcher = []
async def async_added_to_hass(self):
"""Subscribe device events."""
self.unsub_dispatcher.append(async_dispatcher_connect(
self.hass, self.device.event_reachable, self.update_callback))
async def async_will_remove_from_hass(self) -> None:
"""Unsubscribe device events when removed."""
for unsub_dispatcher in self.unsub_dispatcher:
unsub_dispatcher()
@property
def available(self):
"""Return True if device is available."""
return self.device.available
@property
def device_info(self):
"""Return a device description for device registry."""
return {
'identifiers': {(AXIS_DOMAIN, self.device.serial)}
}
@callback
def update_callback(self, no_delay=None):
"""Update the entities state."""
self.async_schedule_update_ha_state()
class AxisEventBase(AxisEntityBase):
"""Base common to all Axis entities from event stream."""
def __init__(self, event, device):
"""Initialize the Axis event."""
super().__init__(device)
self.event = event
async def async_added_to_hass(self) -> None:
"""Subscribe sensors events."""
self.event.register_callback(self.update_callback)
await super().async_added_to_hass()
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
self.event.remove_callback(self.update_callback)
await super().async_will_remove_from_hass()
@property
def device_class(self):
"""Return the class of the event."""
return self.event.CLASS
@property
def name(self):
"""Return the name of the event."""
return '{} {} {}'.format(
self.device.name, self.event.TYPE, self.event.id)
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def unique_id(self):
"""Return a unique identifier for this device."""
return '{}-{}-{}'.format(
self.device.serial, self.event.topic, self.event.id)

View File

@@ -2,6 +2,8 @@
from datetime import timedelta
from axis.event_stream import CLASS_INPUT, CLASS_OUTPUT
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import CONF_MAC, CONF_TRIGGER_TIME
from homeassistant.core import callback
@@ -9,7 +11,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
from .const import DOMAIN as AXIS_DOMAIN, LOGGER
from .axis_base import AxisEventBase
from .const import DOMAIN as AXIS_DOMAIN
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -21,32 +24,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
def async_add_sensor(event_id):
"""Add binary sensor from Axis device."""
event = device.api.event.events[event_id]
async_add_entities([AxisBinarySensor(event, device)], True)
if event.CLASS != CLASS_OUTPUT:
async_add_entities([AxisBinarySensor(event, device)], True)
device.listeners.append(async_dispatcher_connect(
hass, device.event_new_sensor, async_add_sensor))
class AxisBinarySensor(BinarySensorDevice):
class AxisBinarySensor(AxisEventBase, BinarySensorDevice):
"""Representation of a binary Axis event."""
def __init__(self, event, device):
"""Initialize the Axis binary sensor."""
self.event = event
self.device = device
super().__init__(event, device)
self.remove_timer = None
self.unsub_dispatcher = None
async def async_added_to_hass(self):
"""Subscribe sensors events."""
self.event.register_callback(self.update_callback)
self.unsub_dispatcher = async_dispatcher_connect(
self.hass, self.device.event_reachable, self.update_callback)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
self.event.remove_callback(self.update_callback)
self.unsub_dispatcher()
@callback
def update_callback(self, no_delay=False):
@@ -67,7 +59,6 @@ class AxisBinarySensor(BinarySensorDevice):
@callback
def _delay_update(now):
"""Timer callback for sensor update."""
LOGGER.debug("%s called delayed (%s sec) update", self.name, delay)
self.async_schedule_update_ha_state()
self.remove_timer = None
@@ -83,32 +74,10 @@ class AxisBinarySensor(BinarySensorDevice):
@property
def name(self):
"""Return the name of the event."""
return '{} {} {}'.format(
self.device.name, self.event.TYPE, self.event.id)
if self.event.CLASS == CLASS_INPUT and self.event.id and \
self.device.api.vapix.ports[self.event.id].name:
return '{} {}'.format(
self.device.name,
self.device.api.vapix.ports[self.event.id].name)
@property
def device_class(self):
"""Return the class of the event."""
return self.event.CLASS
@property
def unique_id(self):
"""Return a unique identifier for this device."""
return '{}-{}-{}'.format(
self.device.serial, self.event.topic, self.event.id)
def available(self):
"""Return True if device is available."""
return self.device.available
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def device_info(self):
"""Return a device description for device registry."""
return {
'identifiers': {(AXIS_DOMAIN, self.device.serial)}
}
return super().name

View File

@@ -6,9 +6,9 @@ from homeassistant.components.mjpeg.camera import (
from homeassistant.const import (
CONF_AUTHENTICATION, CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME,
CONF_PASSWORD, CONF_PORT, CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .axis_base import AxisEntityBase
from .const import DOMAIN as AXIS_DOMAIN
AXIS_IMAGE = 'http://{}:{}/axis-cgi/jpg/image.cgi'
@@ -38,65 +38,40 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities([AxisCamera(config, device)])
class AxisCamera(MjpegCamera):
class AxisCamera(AxisEntityBase, MjpegCamera):
"""Representation of a Axis camera."""
def __init__(self, config, device):
"""Initialize Axis Communications camera component."""
super().__init__(config)
self.device_config = config
self.device = device
self.port = device.config_entry.data[CONF_DEVICE][CONF_PORT]
self.unsub_dispatcher = []
AxisEntityBase.__init__(self, device)
MjpegCamera.__init__(self, config)
async def async_added_to_hass(self):
"""Subscribe camera events."""
self.unsub_dispatcher.append(async_dispatcher_connect(
self.hass, self.device.event_new_address, self._new_address))
self.unsub_dispatcher.append(async_dispatcher_connect(
self.hass, self.device.event_reachable, self.update_callback))
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
for unsub_dispatcher in self.unsub_dispatcher:
unsub_dispatcher()
await super().async_added_to_hass()
@property
def supported_features(self):
"""Return supported features."""
return SUPPORT_STREAM
@property
def stream_source(self):
async def stream_source(self):
"""Return the stream source."""
return AXIS_STREAM.format(
self.device.config_entry.data[CONF_DEVICE][CONF_USERNAME],
self.device.config_entry.data[CONF_DEVICE][CONF_PASSWORD],
self.device.host)
@callback
def update_callback(self, no_delay=None):
"""Update the cameras state."""
self.async_schedule_update_ha_state()
@property
def available(self):
"""Return True if device is available."""
return self.device.available
def _new_address(self):
"""Set new device address for video stream."""
self._mjpeg_url = AXIS_VIDEO.format(self.device.host, self.port)
self._still_image_url = AXIS_IMAGE.format(self.device.host, self.port)
port = self.device.config_entry.data[CONF_DEVICE][CONF_PORT]
self._mjpeg_url = AXIS_VIDEO.format(self.device.host, port)
self._still_image_url = AXIS_IMAGE.format(self.device.host, port)
@property
def unique_id(self):
"""Return a unique identifier for this device."""
return '{}-camera'.format(self.device.serial)
@property
def device_info(self):
"""Return a device description for device registry."""
return {
'identifiers': {(AXIS_DOMAIN, self.device.serial)}
}

View File

@@ -146,7 +146,7 @@ class AxisFlowHandler(config_entries.ConfigFlow):
entry.data[CONF_DEVICE][CONF_HOST] = host
self.hass.config_entries.async_update_entry(entry)
async def async_step_discovery(self, discovery_info):
async def async_step_zeroconf(self, discovery_info):
"""Prepare configuration for a discovered Axis device.
This flow is triggered by the discovery component.
@@ -155,6 +155,13 @@ class AxisFlowHandler(config_entries.ConfigFlow):
return self.async_abort(reason='link_local_address')
serialnumber = discovery_info['properties']['macaddress']
# pylint: disable=unsupported-assignment-operation
self.context['macaddress'] = serialnumber
if any(serialnumber == flow['context']['macaddress']
for flow in self._async_in_progress()):
return self.async_abort(reason='already_in_progress')
device_entries = configured_devices(self.hass)
if serialnumber in device_entries:

View File

@@ -83,19 +83,23 @@ class AxisNetworkDevice:
self.product_type = self.api.vapix.params.prodtype
if self.config_entry.options[CONF_CAMERA]:
self.hass.async_create_task(
self.hass.config_entries.async_forward_entry_setup(
self.config_entry, 'camera'))
if self.config_entry.options[CONF_EVENTS]:
task = self.hass.async_create_task(
self.hass.config_entries.async_forward_entry_setup(
self.config_entry, 'binary_sensor'))
self.api.stream.connection_status_callback = \
self.async_connection_status_callback
self.api.enable_events(event_callback=self.async_event_callback)
task.add_done_callback(self.start)
platform_tasks = [
self.hass.config_entries.async_forward_entry_setup(
self.config_entry, platform)
for platform in ['binary_sensor', 'switch']
]
self.hass.async_create_task(self.start(platform_tasks))
self.config_entry.add_update_listener(self.async_new_address_callback)
@@ -145,9 +149,9 @@ class AxisNetworkDevice:
if action == 'add':
async_dispatcher_send(self.hass, self.event_new_sensor, event_id)
@callback
def start(self, fut):
"""Start the event stream."""
async def start(self, platform_tasks):
"""Start the event stream when all platforms are loaded."""
await asyncio.gather(*platform_tasks)
self.api.start()
@callback
@@ -157,15 +161,22 @@ class AxisNetworkDevice:
async def async_reset(self):
"""Reset this device to default state."""
self.api.stop()
platform_tasks = []
if self.config_entry.options[CONF_CAMERA]:
await self.hass.config_entries.async_forward_entry_unload(
self.config_entry, 'camera')
platform_tasks.append(
self.hass.config_entries.async_forward_entry_unload(
self.config_entry, 'camera'))
if self.config_entry.options[CONF_EVENTS]:
await self.hass.config_entries.async_forward_entry_unload(
self.config_entry, 'binary_sensor')
self.api.stop()
platform_tasks += [
self.hass.config_entries.async_forward_entry_unload(
self.config_entry, platform)
for platform in ['binary_sensor', 'switch']
]
await asyncio.gather(*platform_tasks)
for unsub_dispatcher in self.listeners:
unsub_dispatcher()
@@ -185,13 +196,22 @@ async def get_device(hass, config):
port=config[CONF_PORT], web_proto='http')
device.vapix.initialize_params(preload_data=False)
device.vapix.initialize_ports()
try:
with async_timeout.timeout(15):
await hass.async_add_executor_job(
device.vapix.params.update_brand)
await hass.async_add_executor_job(
device.vapix.params.update_properties)
await asyncio.gather(
hass.async_add_executor_job(
device.vapix.params.update_brand),
hass.async_add_executor_job(
device.vapix.params.update_properties),
hass.async_add_executor_job(
device.vapix.ports.update)
)
return device
except axis.Unauthorized:

View File

@@ -1,8 +1,10 @@
{
"domain": "axis",
"name": "Axis",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/axis",
"requirements": ["axis==22"],
"requirements": ["axis==24"],
"dependencies": [],
"zeroconf": ["_axis-video._tcp.local."],
"codeowners": ["@kane610"]
}

View File

@@ -14,6 +14,7 @@
},
"error": {
"already_configured": "Device is already configured",
"already_in_progress": "Config flow for device is already in progress.",
"device_unavailable": "Device is not available",
"faulty_credentials": "Bad user credentials"
},

View File

@@ -0,0 +1,59 @@
"""Support for Axis switches."""
from axis.event_stream import CLASS_OUTPUT
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import CONF_MAC
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .axis_base import AxisEventBase
from .const import DOMAIN as AXIS_DOMAIN
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a Axis switch."""
serial_number = config_entry.data[CONF_MAC]
device = hass.data[AXIS_DOMAIN][serial_number]
@callback
def async_add_switch(event_id):
"""Add switch from Axis device."""
event = device.api.event.events[event_id]
if event.CLASS == CLASS_OUTPUT:
async_add_entities([AxisSwitch(event, device)], True)
device.listeners.append(async_dispatcher_connect(
hass, device.event_new_sensor, async_add_switch))
class AxisSwitch(AxisEventBase, SwitchDevice):
"""Representation of a Axis switch."""
@property
def is_on(self):
"""Return true if event is active."""
return self.event.is_tripped
async def async_turn_on(self, **kwargs):
"""Turn on switch."""
action = '/'
await self.hass.async_add_executor_job(
self.device.api.vapix.ports[self.event.id].action, action)
async def async_turn_off(self, **kwargs):
"""Turn off switch."""
action = '\\'
await self.hass.async_add_executor_job(
self.device.api.vapix.ports[self.event.id].action, action)
@property
def name(self):
"""Return the name of the event."""
if self.event.id and self.device.api.vapix.ports[self.event.id].name:
return '{} {}'.format(
self.device.name,
self.device.api.vapix.ports[self.event.id].name)
return super().name

View File

@@ -0,0 +1,80 @@
"""Support for Azure Event Hubs."""
import json
import logging
from typing import Any, Dict
import voluptuous as vol
from azure.eventhub import EventData, EventHubClientAsync
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, STATE_UNAVAILABLE,
STATE_UNKNOWN)
from homeassistant.core import Event, HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
from homeassistant.helpers.json import JSONEncoder
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'azure_event_hub'
CONF_EVENT_HUB_NAMESPACE = 'event_hub_namespace'
CONF_EVENT_HUB_INSTANCE_NAME = 'event_hub_instance_name'
CONF_EVENT_HUB_SAS_POLICY = 'event_hub_sas_policy'
CONF_EVENT_HUB_SAS_KEY = 'event_hub_sas_key'
CONF_FILTER = 'filter'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_EVENT_HUB_NAMESPACE): cv.string,
vol.Required(CONF_EVENT_HUB_INSTANCE_NAME): cv.string,
vol.Required(CONF_EVENT_HUB_SAS_POLICY): cv.string,
vol.Required(CONF_EVENT_HUB_SAS_KEY): cv.string,
vol.Required(CONF_FILTER): FILTER_SCHEMA,
}),
}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
"""Activate Azure EH component."""
config = yaml_config[DOMAIN]
event_hub_address = "amqps://{}.servicebus.windows.net/{}".format(
config[CONF_EVENT_HUB_NAMESPACE],
config[CONF_EVENT_HUB_INSTANCE_NAME])
entities_filter = config[CONF_FILTER]
client = EventHubClientAsync(
event_hub_address,
debug=True,
username=config[CONF_EVENT_HUB_SAS_POLICY],
password=config[CONF_EVENT_HUB_SAS_KEY])
async_sender = client.add_async_sender()
await client.run_async()
encoder = JSONEncoder()
async def async_send_to_event_hub(event: Event):
"""Send states to Event Hub."""
state = event.data.get('new_state')
if (state is None
or state.state in (STATE_UNKNOWN, '', STATE_UNAVAILABLE)
or not entities_filter(state.entity_id)):
return
event_data = EventData(
json.dumps(
obj=state.as_dict(),
default=encoder.encode
).encode('utf-8')
)
await async_sender.send(event_data)
async def async_shutdown(event: Event):
"""Shut down the client."""
await client.stop_async()
hass.bus.async_listen(EVENT_STATE_CHANGED, async_send_to_event_hub)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_shutdown)
return True

View File

@@ -0,0 +1,8 @@
{
"domain": "azure_event_hub",
"name": "Azure Event Hub",
"documentation": "https://www.home-assistant.io/components/azure_event_hub",
"requirements": ["azure-eventhub==1.3.1"],
"dependencies": [],
"codeowners": ["@eavanvalkenburg"]
}

View File

@@ -8,7 +8,7 @@ from homeassistant.helpers import (
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_SCAN_INTERVAL,
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
CONF_MONITORED_CONDITIONS, CONF_MODE, CONF_OFFSET, TEMP_FAHRENHEIT)
_LOGGER = logging.getLogger(__name__)
@@ -41,7 +41,7 @@ BINARY_SENSORS = {
SENSORS = {
TYPE_TEMPERATURE: ['Temperature', TEMP_FAHRENHEIT, 'mdi:thermometer'],
TYPE_BATTERY: ['Battery', '%', 'mdi:battery-80'],
TYPE_BATTERY: ['Battery', '', 'mdi:battery-80'],
TYPE_WIFI_STRENGTH: ['Wifi Signal', 'dBm', 'mdi:wifi-strength-2'],
}
@@ -75,6 +75,8 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_BINARY_SENSORS, default={}):
BINARY_SENSOR_SCHEMA,
vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
vol.Optional(CONF_OFFSET, default=1): int,
vol.Optional(CONF_MODE, default=''): cv.string,
})
},
extra=vol.ALLOW_EXTRA)
@@ -87,8 +89,12 @@ def setup(hass, config):
username = conf[CONF_USERNAME]
password = conf[CONF_PASSWORD]
scan_interval = conf[CONF_SCAN_INTERVAL]
is_legacy = bool(conf[CONF_MODE] == 'legacy')
motion_interval = conf[CONF_OFFSET]
hass.data[BLINK_DATA] = blinkpy.Blink(username=username,
password=password)
password=password,
motion_interval=motion_interval,
legacy_subdomain=is_legacy)
hass.data[BLINK_DATA].refresh_rate = scan_interval.total_seconds()
hass.data[BLINK_DATA].start()

View File

@@ -3,7 +3,7 @@
"name": "Blink",
"documentation": "https://www.home-assistant.io/components/blink",
"requirements": [
"blinkpy==0.13.1"
"blinkpy==0.14.0"
],
"dependencies": [],
"codeowners": [

View File

@@ -255,7 +255,7 @@ class BluesoundPlayer(MediaPlayerDevice):
BluesoundPlayer._TimeoutException):
_LOGGER.info("Node %s is offline, retrying later", self._name)
await asyncio.sleep(
NODE_OFFLINE_CHECK_TIMEOUT, loop=self._hass.loop)
NODE_OFFLINE_CHECK_TIMEOUT)
self.start_polling()
except CancelledError:
@@ -318,7 +318,7 @@ class BluesoundPlayer(MediaPlayerDevice):
try:
websession = async_get_clientsession(self._hass)
with async_timeout.timeout(10, loop=self._hass.loop):
with async_timeout.timeout(10):
response = await websession.get(url)
if response.status == 200:
@@ -361,7 +361,7 @@ class BluesoundPlayer(MediaPlayerDevice):
try:
with async_timeout.timeout(125, loop=self._hass.loop):
with async_timeout.timeout(125):
response = await self._polling_session.get(
url, headers={CONNECTION: KEEP_ALIVE})
@@ -378,7 +378,7 @@ class BluesoundPlayer(MediaPlayerDevice):
self._group_name = group_name
# the sleep is needed to make sure that the
# devices is synced
await asyncio.sleep(1, loop=self._hass.loop)
await asyncio.sleep(1)
await self.async_trigger_sync_on_all()
elif self.is_grouped:
# when player is grouped we need to fetch volume from

View File

@@ -2,12 +2,15 @@
import logging
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.components.device_tracker import (
YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
load_config, SOURCE_TYPE_BLUETOOTH_LE
from homeassistant.components.device_tracker.legacy import (
YAML_DEVICES, async_load_config
)
from homeassistant.components.device_tracker.const import (
CONF_TRACK_NEW, CONF_SCAN_INTERVAL, SCAN_INTERVAL, SOURCE_TYPE_BLUETOOTH_LE
)
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
import homeassistant.util.dt as dt_util
from homeassistant.util.async_ import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__)
@@ -79,7 +82,10 @@ def setup_scanner(hass, config, see, discovery_info=None):
# Load all known devices.
# We just need the devices so set consider_home and home range
# to 0
for device in load_config(yaml_path, hass, 0):
for device in run_coroutine_threadsafe(
async_load_config(yaml_path, hass, 0),
hass.loop
).result():
# check if device is a valid bluetooth device
if device.mac and device.mac[:4].upper() == BLE_PREFIX:
if device.track:
@@ -97,7 +103,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
_LOGGER.warning("No Bluetooth LE devices to track!")
return False
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
def update_ble(now):
"""Lookup Bluetooth LE devices and update status."""

View File

@@ -5,11 +5,16 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.components.device_tracker import (
YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
load_config, PLATFORM_SCHEMA, DEFAULT_TRACK_NEW, SOURCE_TYPE_BLUETOOTH,
DOMAIN)
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
from homeassistant.components.device_tracker.legacy import (
YAML_DEVICES, async_load_config
)
from homeassistant.components.device_tracker.const import (
CONF_TRACK_NEW, CONF_SCAN_INTERVAL, SCAN_INTERVAL, DEFAULT_TRACK_NEW,
SOURCE_TYPE_BLUETOOTH, DOMAIN
)
import homeassistant.util.dt as dt_util
from homeassistant.util.async_ import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__)
@@ -60,7 +65,10 @@ def setup_scanner(hass, config, see, discovery_info=None):
# Load all known devices.
# We just need the devices so set consider_home and home range
# to 0
for device in load_config(yaml_path, hass, 0):
for device in run_coroutine_threadsafe(
async_load_config(yaml_path, hass, 0),
hass.loop
).result():
# Check if device is a valid bluetooth device
if device.mac and device.mac[:3].upper() == BT_PREFIX:
if device.track:
@@ -77,7 +85,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
devs_to_track.append(dev[0])
see_device(dev[0], dev[1])
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
request_rssi = config.get(CONF_REQUEST_RSSI, False)

View File

@@ -3,7 +3,7 @@
"name": "Broadlink",
"documentation": "https://www.home-assistant.io/components/broadlink",
"requirements": [
"broadlink==0.9.0"
"broadlink==0.10.0"
],
"dependencies": [],
"codeowners": [

View File

@@ -1,7 +1,6 @@
"""Support for the Broadlink RM2 Pro (only temperature) and A1 devices."""
import binascii
import logging
import socket
from datetime import timedelta
import voluptuous as vol
@@ -60,6 +59,7 @@ class BroadlinkSensor(Entity):
"""Initialize the sensor."""
self._name = '{} {}'.format(name, SENSOR_TYPES[sensor_type][0])
self._state = None
self._is_available = False
self._type = sensor_type
self._broadlink_data = broadlink_data
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
@@ -74,6 +74,11 @@ class BroadlinkSensor(Entity):
"""Return the state of the sensor."""
return self._state
@property
def available(self):
"""Return True if entity is available."""
return self._is_available
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
@@ -83,8 +88,11 @@ class BroadlinkSensor(Entity):
"""Get the latest data from the sensor."""
self._broadlink_data.update()
if self._broadlink_data.data is None:
self._state = None
self._is_available = False
return
self._state = self._broadlink_data.data[self._type]
self._is_available = True
class BroadlinkData:
@@ -119,8 +127,9 @@ class BroadlinkData:
if data is not None:
self.data = self._schema(data)
return
except socket.timeout as error:
except OSError as error:
if retry < 1:
self.data = None
_LOGGER.error(error)
return
except (vol.Invalid, vol.MultipleInvalid):
@@ -131,7 +140,7 @@ class BroadlinkData:
def _auth(self, retry=3):
try:
auth = self._device.auth()
except socket.timeout:
except OSError:
auth = False
if not auth and retry > 0:
self._connect()

View File

@@ -10,9 +10,10 @@ from homeassistant.components.switch import (
ENTITY_ID_FORMAT, PLATFORM_SCHEMA, SwitchDevice)
from homeassistant.const import (
CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_FRIENDLY_NAME, CONF_HOST, CONF_MAC,
CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE)
CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE, STATE_ON)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle, slugify
from homeassistant.helpers.restore_state import RestoreEntity
from . import async_setup_service, data_packet
@@ -109,13 +110,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
broadlink_device.timeout = config.get(CONF_TIMEOUT)
try:
broadlink_device.auth()
except socket.timeout:
except OSError:
_LOGGER.error("Failed to connect to device")
add_entities(switches)
class BroadlinkRMSwitch(SwitchDevice):
class BroadlinkRMSwitch(SwitchDevice, RestoreEntity):
"""Representation of an Broadlink switch."""
def __init__(self, name, friendly_name, device, command_on, command_off):
@@ -126,6 +127,14 @@ class BroadlinkRMSwitch(SwitchDevice):
self._command_on = command_on
self._command_off = command_off
self._device = device
self._is_available = False
async def async_added_to_hass(self):
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
state = await self.async_get_last_state()
if state:
self._state = state.state == STATE_ON
@property
def name(self):
@@ -137,6 +146,11 @@ class BroadlinkRMSwitch(SwitchDevice):
"""Return true if unable to access real state of entity."""
return True
@property
def available(self):
"""Return True if entity is available."""
return not self.should_poll or self._is_available
@property
def should_poll(self):
"""Return the polling state."""
@@ -166,7 +180,7 @@ class BroadlinkRMSwitch(SwitchDevice):
return True
try:
self._device.send_data(packet)
except (socket.timeout, ValueError) as error:
except (ValueError, OSError) as error:
if retry < 1:
_LOGGER.error("Error during sending a packet: %s", error)
return False
@@ -178,7 +192,7 @@ class BroadlinkRMSwitch(SwitchDevice):
def _auth(self, retry=2):
try:
auth = self._device.auth()
except socket.timeout:
except OSError:
auth = False
if retry < 1:
_LOGGER.error("Timeout during authorization")
@@ -244,6 +258,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch):
except (socket.timeout, ValueError) as error:
if retry < 1:
_LOGGER.error("Error during updating the state: %s", error)
self._is_available = False
return
if not self._auth():
return
@@ -252,6 +267,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch):
return self._update(retry-1)
self._state = state
self._load_power = load_power
self._is_available = True
class BroadlinkMP1Slot(BroadlinkRMSwitch):
@@ -277,10 +293,12 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch):
except (socket.timeout, ValueError) as error:
if retry < 1:
_LOGGER.error("Error during sending a packet: %s", error)
self._is_available = False
return False
if not self._auth():
return False
return self._sendpacket(packet, max(0, retry-1))
self._is_available = True
return True
@property
@@ -330,7 +348,7 @@ class BroadlinkMP1Switch:
"""Authenticate the device."""
try:
auth = self._device.auth()
except socket.timeout:
except OSError:
auth = False
if not auth and retry > 0:
return self._auth(retry-1)

View File

@@ -388,7 +388,7 @@ class BrData:
tasks.append(dev.async_update_ha_state())
if tasks:
await asyncio.wait(tasks, loop=self.hass.loop)
await asyncio.wait(tasks)
async def schedule_update(self, minute=1):
"""Schedule an update after minute minutes."""
@@ -407,7 +407,7 @@ class BrData:
resp = None
try:
websession = async_get_clientsession(self.hass)
with async_timeout.timeout(10, loop=self.hass.loop):
with async_timeout.timeout(10):
resp = await websession.get(url)
result[STATUS_CODE] = resp.status

View File

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

View File

@@ -107,11 +107,14 @@ async def async_request_stream(hass, entity_id, fmt):
camera = _get_camera_from_entity_id(hass, entity_id)
camera_prefs = hass.data[DATA_CAMERA_PREFS].get(entity_id)
if not camera.stream_source:
async with async_timeout.timeout(10):
source = await camera.stream_source()
if not source:
raise HomeAssistantError("{} does not support play stream service"
.format(camera.entity_id))
return request_stream(hass, camera.stream_source, fmt=fmt,
return request_stream(hass, source, fmt=fmt,
keepalive=camera_prefs.preload_stream)
@@ -121,7 +124,7 @@ async def async_get_image(hass, entity_id, timeout=10):
camera = _get_camera_from_entity_id(hass, entity_id)
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(timeout, loop=hass.loop):
async with async_timeout.timeout(timeout):
image = await camera.async_camera_image()
if image:
@@ -221,8 +224,16 @@ async def async_setup(hass, config):
async def preload_stream(hass, _):
for camera in component.entities:
camera_prefs = prefs.get(camera.entity_id)
if camera.stream_source and camera_prefs.preload_stream:
request_stream(hass, camera.stream_source, keepalive=True)
if not camera_prefs.preload_stream:
continue
async with async_timeout.timeout(10):
source = await camera.stream_source()
if not source:
continue
request_stream(hass, source, keepalive=True)
async_when_setup(hass, DOMAIN_STREAM, preload_stream)
@@ -328,8 +339,7 @@ class Camera(Entity):
"""Return the interval between frames of the mjpeg stream."""
return 0.5
@property
def stream_source(self):
async def stream_source(self):
"""Return the source of the stream."""
return None
@@ -481,7 +491,7 @@ class CameraImageView(CameraView):
async def handle(self, request, camera):
"""Serve camera image."""
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(10, loop=request.app['hass'].loop):
async with async_timeout.timeout(10):
image = await camera.async_camera_image()
if image:
@@ -522,12 +532,10 @@ async def websocket_camera_thumbnail(hass, connection, msg):
"""
try:
image = await async_get_image(hass, msg['entity_id'])
connection.send_message(websocket_api.result_message(
msg['id'], {
'content_type': image.content_type,
'content': base64.b64encode(image.content).decode('utf-8')
}
))
await connection.send_big_result(msg['id'], {
'content_type': image.content_type,
'content': base64.b64encode(image.content).decode('utf-8')
})
except HomeAssistantError:
connection.send_message(websocket_api.error_message(
msg['id'], 'image_fetch_failed', 'Unable to fetch image'))
@@ -549,18 +557,25 @@ async def ws_camera_stream(hass, connection, msg):
camera = _get_camera_from_entity_id(hass, entity_id)
camera_prefs = hass.data[DATA_CAMERA_PREFS].get(entity_id)
if not camera.stream_source:
async with async_timeout.timeout(10):
source = await camera.stream_source()
if not 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,
url = request_stream(hass, source, fmt=fmt,
keepalive=camera_prefs.preload_stream)
connection.send_result(msg['id'], {'url': url})
except HomeAssistantError as ex:
_LOGGER.error(ex)
_LOGGER.error("Error requesting stream: %s", ex)
connection.send_error(
msg['id'], 'start_stream_failed', str(ex))
except asyncio.TimeoutError:
_LOGGER.error("Timeout getting stream source")
connection.send_error(
msg['id'], 'start_stream_failed', "Timeout getting stream source")
@websocket_api.async_response
@@ -624,7 +639,10 @@ async def async_handle_snapshot_service(camera, service):
async def async_handle_play_stream_service(camera, service_call):
"""Handle play stream services calls."""
if not camera.stream_source:
async with async_timeout.timeout(10):
source = await camera.stream_source()
if not source:
raise HomeAssistantError("{} does not support play stream service"
.format(camera.entity_id))
@@ -633,7 +651,7 @@ async def async_handle_play_stream_service(camera, service_call):
fmt = service_call.data[ATTR_FORMAT]
entity_ids = service_call.data[ATTR_MEDIA_PLAYER]
url = request_stream(hass, camera.stream_source, fmt=fmt,
url = request_stream(hass, source, fmt=fmt,
keepalive=camera_prefs.preload_stream)
data = {
ATTR_ENTITY_ID: entity_ids,
@@ -648,7 +666,10 @@ async def async_handle_play_stream_service(camera, service_call):
async def async_handle_record_service(camera, call):
"""Handle stream recording service calls."""
if not camera.stream_source:
async with async_timeout.timeout(10):
source = await camera.stream_source()
if not source:
raise HomeAssistantError("{} does not support record service"
.format(camera.entity_id))
@@ -659,7 +680,7 @@ async def async_handle_record_service(camera, call):
variables={ATTR_ENTITY_ID: camera})
data = {
CONF_STREAM_SOURCE: camera.stream_source,
CONF_STREAM_SOURCE: source,
CONF_FILENAME: video_path,
CONF_DURATION: call.data[CONF_DURATION],
CONF_LOOKBACK: call.data[CONF_LOOKBACK],

View File

@@ -79,7 +79,7 @@ class CanaryCamera(Camera):
image = await asyncio.shield(ffmpeg.get_image(
self._live_stream_session.live_stream_url,
output_format=IMAGE_JPEG,
extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
extra_cmd=self._ffmpeg_arguments))
return image
async def handle_async_mjpeg_stream(self, request):

View File

@@ -1,8 +1,7 @@
"""Component to embed Google Cast."""
from homeassistant import config_entries
from homeassistant.helpers import config_entry_flow
DOMAIN = 'cast'
from .const import DOMAIN
async def async_setup(hass, config):
@@ -23,15 +22,3 @@ async def async_setup_entry(hass, entry):
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
entry, 'media_player'))
return True
async def _async_has_devices(hass):
"""Return if there are devices that can be discovered."""
from pychromecast.discovery import discover_chromecasts
return await hass.async_add_executor_job(discover_chromecasts)
config_entry_flow.register_discovery_flow(
DOMAIN, 'Google Cast', _async_has_devices,
config_entries.CONN_CLASS_LOCAL_PUSH)

View File

@@ -0,0 +1,16 @@
"""Config flow for Cast."""
from homeassistant.helpers import config_entry_flow
from homeassistant import config_entries
from .const import DOMAIN
async def _async_has_devices(hass):
"""Return if there are devices that can be discovered."""
from pychromecast.discovery import discover_chromecasts
return await hass.async_add_executor_job(discover_chromecasts)
config_entry_flow.register_discovery_flow(
DOMAIN, 'Google Cast', _async_has_devices,
config_entries.CONN_CLASS_LOCAL_PUSH)

View File

@@ -0,0 +1,3 @@
"""Consts for Cast integration."""
DOMAIN = 'cast'

View File

@@ -1,6 +1,7 @@
{
"domain": "cast",
"name": "Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/cast",
"requirements": [
"pychromecast==3.2.1"

View File

@@ -106,7 +106,7 @@ async def async_citybikes_request(hass, uri, schema):
try:
session = async_get_clientsession(hass)
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
req = await session.get(DEFAULT_ENDPOINT.format(uri=uri))
json_response = await req.json()
@@ -181,7 +181,7 @@ class CityBikesNetworks:
"""Initialize the networks instance."""
self.hass = hass
self.networks = None
self.networks_loading = asyncio.Condition(loop=hass.loop)
self.networks_loading = asyncio.Condition()
async def get_closest_network_id(self, latitude, longitude):
"""Return the id of the network closest to provided location."""
@@ -217,7 +217,7 @@ class CityBikesNetwork:
self.hass = hass
self.network_id = network_id
self.stations = []
self.ready = asyncio.Event(loop=hass.loop)
self.ready = asyncio.Event()
async def async_refresh(self, now=None):
"""Refresh the state of the network."""

View File

@@ -17,7 +17,9 @@ from homeassistant.util.aiohttp import MockRequest
from . import utils
from .const import (
CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE)
CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE,
PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE,
PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
from .prefs import CloudPreferences
@@ -98,12 +100,26 @@ class CloudClient(Interface):
if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False
return google_conf['filter'](entity.entity_id)
if not google_conf['filter'].empty_filter:
return google_conf['filter'](entity.entity_id)
entity_configs = self.prefs.google_entity_configs
entity_config = entity_configs.get(entity.entity_id, {})
return entity_config.get(
PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE)
def should_2fa(entity):
"""If an entity should be checked for 2FA."""
entity_configs = self.prefs.google_entity_configs
entity_config = entity_configs.get(entity.entity_id, {})
return not entity_config.get(
PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
username = self._hass.data[DOMAIN].claims["cognito:username"]
self._google_config = ga_h.Config(
should_expose=should_expose,
should_2fa=should_2fa,
secure_devices_pin=self._prefs.google_secure_devices_pin,
entity_config=google_conf.get(CONF_ENTITY_CONFIG),
agent_user_id=username,

View File

@@ -8,6 +8,13 @@ PREF_ENABLE_REMOTE = 'remote_enabled'
PREF_GOOGLE_SECURE_DEVICES_PIN = 'google_secure_devices_pin'
PREF_CLOUDHOOKS = 'cloudhooks'
PREF_CLOUD_USER = 'cloud_user'
PREF_GOOGLE_ENTITY_CONFIGS = 'google_entity_configs'
PREF_OVERRIDE_NAME = 'override_name'
PREF_DISABLE_2FA = 'disable_2fa'
PREF_ALIASES = 'aliases'
PREF_SHOULD_EXPOSE = 'should_expose'
DEFAULT_SHOULD_EXPOSE = True
DEFAULT_DISABLE_2FA = False
CONF_ALEXA = 'alexa'
CONF_ALIASES = 'aliases'

View File

@@ -14,8 +14,7 @@ from homeassistant.components.http.data_validator import (
RequestDataValidator)
from homeassistant.components import websocket_api
from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import (
const as google_const)
from homeassistant.components.google_assistant import helpers as google_helpers
from .const import (
DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
@@ -81,6 +80,12 @@ async def async_setup(hass):
websocket_remote_connect)
hass.components.websocket_api.async_register_command(
websocket_remote_disconnect)
hass.components.websocket_api.async_register_command(
google_assistant_list)
hass.components.websocket_api.async_register_command(
google_assistant_update)
hass.http.register_view(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView)
@@ -164,10 +169,10 @@ class GoogleActionsSyncView(HomeAssistantView):
cloud = hass.data[DOMAIN]
websession = hass.helpers.aiohttp_client.async_get_clientsession()
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
await hass.async_add_job(cloud.auth.check_token)
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
req = await websession.post(
cloud.google_actions_sync_url, headers={
'authorization': cloud.id_token
@@ -192,7 +197,7 @@ class CloudLoginView(HomeAssistantView):
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
await hass.async_add_job(cloud.auth.login, data['email'],
data['password'])
@@ -212,7 +217,7 @@ class CloudLogoutView(HomeAssistantView):
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
await cloud.logout()
return self.json_message('ok')
@@ -234,7 +239,7 @@ class CloudRegisterView(HomeAssistantView):
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
await hass.async_add_job(
cloud.auth.register, data['email'], data['password'])
@@ -256,7 +261,7 @@ class CloudResendConfirmView(HomeAssistantView):
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
await hass.async_add_job(
cloud.auth.resend_email_confirm, data['email'])
@@ -278,7 +283,7 @@ class CloudForgotPasswordView(HomeAssistantView):
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
await hass.async_add_job(
cloud.auth.forgot_password, data['email'])
@@ -320,7 +325,7 @@ async def websocket_subscription(hass, connection, msg):
from hass_nabucasa.const import STATE_DISCONNECTED
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
response = await cloud.fetch_subscription_info()
if response.status != 200:
@@ -411,7 +416,6 @@ def _account_data(cloud):
'cloud': cloud.iot.state,
'prefs': client.prefs.as_dict(),
'google_entities': client.google_user_config['filter'].config,
'google_domains': list(google_const.DOMAIN_TO_GOOGLE_TYPES),
'alexa_entities': client.alexa_config.should_expose.config,
'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS),
'remote_domain': remote.instance_domain,
@@ -448,3 +452,55 @@ async def websocket_remote_disconnect(hass, connection, msg):
await cloud.client.prefs.async_update(remote_enabled=False)
await cloud.remote.disconnect()
connection.send_result(msg['id'], _account_data(cloud))
@websocket_api.require_admin
@_require_cloud_login
@websocket_api.async_response
@_ws_handle_cloud_errors
@websocket_api.websocket_command({
'type': 'cloud/google_assistant/entities'
})
async def google_assistant_list(hass, connection, msg):
"""List all google assistant entities."""
cloud = hass.data[DOMAIN]
entities = google_helpers.async_get_entities(
hass, cloud.client.google_config
)
result = []
for entity in entities:
result.append({
'entity_id': entity.entity_id,
'traits': [trait.name for trait in entity.traits()],
'might_2fa': entity.might_2fa(),
})
connection.send_result(msg['id'], result)
@websocket_api.require_admin
@_require_cloud_login
@websocket_api.async_response
@_ws_handle_cloud_errors
@websocket_api.websocket_command({
'type': 'cloud/google_assistant/entities/update',
'entity_id': str,
vol.Optional('should_expose'): bool,
vol.Optional('override_name'): str,
vol.Optional('aliases'): [str],
vol.Optional('disable_2fa'): bool,
})
async def google_assistant_update(hass, connection, msg):
"""List all google assistant entities."""
cloud = hass.data[DOMAIN]
changes = dict(msg)
changes.pop('type')
changes.pop('id')
await cloud.client.prefs.async_update_google_entity_config(**changes)
connection.send_result(
msg['id'],
cloud.client.prefs.google_entity_configs.get(msg['entity_id']))

View File

@@ -3,7 +3,7 @@
"name": "Cloud",
"documentation": "https://www.home-assistant.io/components/cloud",
"requirements": [
"hass-nabucasa==0.12"
"hass-nabucasa==0.13"
],
"dependencies": [
"http",

View File

@@ -4,6 +4,8 @@ from ipaddress import ip_address
from .const import (
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE,
PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER,
PREF_GOOGLE_ENTITY_CONFIGS, PREF_OVERRIDE_NAME, PREF_DISABLE_2FA,
PREF_ALIASES, PREF_SHOULD_EXPOSE,
InvalidTrustedNetworks)
STORAGE_KEY = DOMAIN
@@ -30,6 +32,7 @@ class CloudPreferences:
PREF_ENABLE_GOOGLE: True,
PREF_ENABLE_REMOTE: False,
PREF_GOOGLE_SECURE_DEVICES_PIN: None,
PREF_GOOGLE_ENTITY_CONFIGS: {},
PREF_CLOUDHOOKS: {},
PREF_CLOUD_USER: None,
}
@@ -39,7 +42,7 @@ class CloudPreferences:
async def async_update(self, *, google_enabled=_UNDEF,
alexa_enabled=_UNDEF, remote_enabled=_UNDEF,
google_secure_devices_pin=_UNDEF, cloudhooks=_UNDEF,
cloud_user=_UNDEF):
cloud_user=_UNDEF, google_entity_configs=_UNDEF):
"""Update user preferences."""
for key, value in (
(PREF_ENABLE_GOOGLE, google_enabled),
@@ -48,6 +51,7 @@ class CloudPreferences:
(PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin),
(PREF_CLOUDHOOKS, cloudhooks),
(PREF_CLOUD_USER, cloud_user),
(PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs),
):
if value is not _UNDEF:
self._prefs[key] = value
@@ -57,9 +61,48 @@ class CloudPreferences:
await self._store.async_save(self._prefs)
async def async_update_google_entity_config(
self, *, entity_id, override_name=_UNDEF, disable_2fa=_UNDEF,
aliases=_UNDEF, should_expose=_UNDEF):
"""Update config for a Google entity."""
entities = self.google_entity_configs
entity = entities.get(entity_id, {})
changes = {}
for key, value in (
(PREF_OVERRIDE_NAME, override_name),
(PREF_DISABLE_2FA, disable_2fa),
(PREF_ALIASES, aliases),
(PREF_SHOULD_EXPOSE, should_expose),
):
if value is not _UNDEF:
changes[key] = value
if not changes:
return
updated_entity = {
**entity,
**changes,
}
updated_entities = {
**entities,
entity_id: updated_entity,
}
await self.async_update(google_entity_configs=updated_entities)
def as_dict(self):
"""Return dictionary version."""
return self._prefs
return {
PREF_ENABLE_ALEXA: self.alexa_enabled,
PREF_ENABLE_GOOGLE: self.google_enabled,
PREF_ENABLE_REMOTE: self.remote_enabled,
PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin,
PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs,
PREF_CLOUDHOOKS: self.cloudhooks,
PREF_CLOUD_USER: self.cloud_user,
}
@property
def remote_enabled(self):
@@ -89,6 +132,11 @@ class CloudPreferences:
"""Return if Google is allowed to unlock locks."""
return self._prefs.get(PREF_GOOGLE_SECURE_DEVICES_PIN)
@property
def google_entity_configs(self):
"""Return Google Entity configurations."""
return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {})
@property
def cloudhooks(self):
"""Return the published cloud webhooks."""

View File

@@ -1,13 +1,25 @@
"""Helper functions for cloud components."""
from typing import Any, Dict
from aiohttp import web
from aiohttp import web, payload
def aiohttp_serialize_response(response: web.Response) -> Dict[str, Any]:
"""Serialize an aiohttp response to a dictionary."""
body = response.body
if body is None:
pass
elif isinstance(body, payload.StringPayload):
# pylint: disable=protected-access
body = body._value.decode(body.encoding)
elif isinstance(body, bytes):
body = body.decode(response.charset or 'utf-8')
else:
raise ValueError("Unknown payload encoding")
return {
'status': response.status,
'body': response.text,
'body': body,
'headers': dict(response.headers),
}

View File

@@ -106,7 +106,7 @@ class ComedHourlyPricingSensor(Entity):
else:
url_string += '?type=currenthouraverage'
with async_timeout.timeout(60, loop=self.loop):
with async_timeout.timeout(60):
response = await self.websession.get(url_string)
# The API responds with MIME type 'text/html'
text = await response.text()

View File

@@ -30,7 +30,7 @@ ON_DEMAND = ('zwave',)
async def async_setup(hass, config):
"""Set up the config component."""
await hass.components.frontend.async_register_built_in_panel(
hass.components.frontend.async_register_built_in_panel(
'config', 'config', 'hass:settings', require_admin=True)
async def setup_panel(panel_name):
@@ -62,7 +62,7 @@ async def async_setup(hass, config):
tasks.append(setup_panel(panel_name))
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks)
return True
@@ -92,6 +92,10 @@ class BaseEditConfigView(HomeAssistantView):
"""Set value."""
raise NotImplementedError
def _delete_value(self, hass, data, config_key):
"""Delete value."""
raise NotImplementedError
async def get(self, request, config_key):
"""Fetch device specific config."""
hass = request.app['hass']
@@ -128,7 +132,27 @@ class BaseEditConfigView(HomeAssistantView):
current = await self.read_config(hass)
self._write_value(hass, current, config_key, data)
await hass.async_add_job(_write, path, current)
await hass.async_add_executor_job(_write, path, current)
if self.post_write_hook is not None:
hass.async_create_task(self.post_write_hook(hass))
return self.json({
'result': 'ok',
})
async def delete(self, request, config_key):
"""Remove an entry."""
hass = request.app['hass']
current = await self.read_config(hass)
value = self._get_value(hass, current, config_key)
path = hass.config.path(self.path)
if value is None:
return self.json_message('Resource not found', 404)
self._delete_value(hass, current, config_key)
await hass.async_add_executor_job(_write, path, current)
if self.post_write_hook is not None:
hass.async_create_task(self.post_write_hook(hass))
@@ -161,6 +185,10 @@ class EditKeyBasedConfigView(BaseEditConfigView):
"""Set value."""
data.setdefault(config_key, {}).update(new_value)
def _delete_value(self, hass, data, config_key):
"""Delete value."""
return data.pop(config_key)
class EditIdBasedConfigView(BaseEditConfigView):
"""Configure key based config entries."""
@@ -184,6 +212,13 @@ class EditIdBasedConfigView(BaseEditConfigView):
value.update(new_value)
def _delete_value(self, hass, data, config_key):
"""Delete value."""
index = next(
idx for idx, val in enumerate(data)
if val.get(CONF_ID) == config_key)
data.pop(index)
def _read(path):
"""Read YAML helper."""

View File

@@ -6,6 +6,7 @@ from homeassistant.components.http import HomeAssistantView
from homeassistant.exceptions import Unauthorized
from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView, FlowManagerResourceView)
from homeassistant.generated import config_flows
async def async_setup(hass):
@@ -172,7 +173,7 @@ class ConfigManagerAvailableFlowView(HomeAssistantView):
async def get(self, request):
"""List available flow handlers."""
return self.json(config_entries.FLOWS)
return self.json(config_flows.FLOWS)
class OptionManagerFlowIndexView(FlowManagerIndexView):

View File

@@ -1,12 +1,22 @@
"""Component to interact with Hassbian tools."""
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
from homeassistant.config import async_check_ha_config_file
from homeassistant.components import websocket_api
from homeassistant.const import (
CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL
)
from homeassistant.helpers import config_validation as cv
from homeassistant.util import location
async def async_setup(hass):
"""Set up the Hassbian config."""
hass.http.register_view(CheckConfigView)
websocket_api.async_register_command(hass, websocket_update_config)
websocket_api.async_register_command(hass, websocket_detect_config)
return True
@@ -26,3 +36,62 @@ class CheckConfigView(HomeAssistantView):
"result": state,
"errors": errors,
})
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command({
'type': 'config/core/update',
vol.Optional('latitude'): cv.latitude,
vol.Optional('longitude'): cv.longitude,
vol.Optional('elevation'): int,
vol.Optional('unit_system'): cv.unit_system,
vol.Optional('location_name'): str,
vol.Optional('time_zone'): cv.time_zone,
})
async def websocket_update_config(hass, connection, msg):
"""Handle update core config command."""
data = dict(msg)
data.pop('id')
data.pop('type')
try:
await hass.config.update(**data)
connection.send_result(msg['id'])
except ValueError as err:
connection.send_error(
msg['id'], 'invalid_info', str(err)
)
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command({
'type': 'config/core/detect',
})
async def websocket_detect_config(hass, connection, msg):
"""Detect core config."""
session = hass.helpers.aiohttp_client.async_get_clientsession()
location_info = await location.async_detect_location_info(session)
info = {}
if location_info is None:
connection.send_result(msg['id'], info)
return
if location_info.use_metric:
info['unit_system'] = CONF_UNIT_SYSTEM_METRIC
else:
info['unit_system'] = CONF_UNIT_SYSTEM_IMPERIAL
if location_info.latitude:
info['latitude'] = location_info.latitude
if location_info.longitude:
info['longitude'] = location_info.longitude
if location_info.time_zone:
info['time_zone'] = location_info.time_zone
connection.send_result(msg['id'], info)

View File

@@ -81,12 +81,7 @@ class DaikinClimate(ClimateDevice):
self._api = api
self._list = {
ATTR_OPERATION_MODE: list(HA_STATE_TO_DAIKIN),
ATTR_FAN_MODE: list(
map(
str.title,
appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE])
)
),
ATTR_FAN_MODE: self._api.device.fan_rate,
ATTR_SWING_MODE: list(
map(
str.title,
@@ -95,11 +90,14 @@ class DaikinClimate(ClimateDevice):
),
}
self._supported_features = (SUPPORT_AWAY_MODE | SUPPORT_ON_OFF
self._supported_features = (SUPPORT_ON_OFF
| SUPPORT_OPERATION_MODE
| SUPPORT_TARGET_TEMPERATURE)
if self._api.device.support_fan_mode:
if self._api.device.support_away_mode:
self._supported_features |= SUPPORT_AWAY_MODE
if self._api.device.support_fan_rate:
self._supported_features |= SUPPORT_FAN_MODE
if self._api.device.support_swing_mode:

View File

@@ -1,9 +1,10 @@
{
"domain": "daikin",
"name": "Daikin",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/daikin",
"requirements": [
"pydaikin==1.4.0"
"pydaikin==1.4.6"
],
"dependencies": [],
"codeowners": [

View File

@@ -26,9 +26,12 @@ async def async_setup_platform(
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Daikin climate based on config_entry."""
daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id)
sensors = [ATTR_INSIDE_TEMPERATURE]
if daikin_api.device.support_outside_temperature:
sensors.append(ATTR_OUTSIDE_TEMPERATURE)
async_add_entities([
DaikinClimateSensor(daikin_api, sensor, hass.config.units)
for sensor in SENSOR_TYPES
for sensor in sensors
])

View File

@@ -27,8 +27,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
if zones:
async_add_entities([
DaikinZoneSwitch(daikin_api, zone_id)
for zone_id, name in enumerate(zones)
if name != '-'
for zone_id, zone in enumerate(zones) if zone != ('-', '0')
])

View File

@@ -103,6 +103,8 @@ class DarkSkyWeather(WeatherEntity):
@property
def temperature_unit(self):
"""Return the unit of measurement."""
if self._dark_sky.units is None:
return None
return TEMP_FAHRENHEIT if 'us' in self._dark_sky.units \
else TEMP_CELSIUS

View File

@@ -3,12 +3,21 @@
"abort": {
"already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9",
"no_bridges": "Aucun pont deCONZ n'a \u00e9t\u00e9 d\u00e9couvert",
"one_instance_only": "Le composant prend uniquement en charge une instance deCONZ"
"one_instance_only": "Le composant prend uniquement en charge une instance deCONZ",
"updated_instance": "Instance deCONZ mise \u00e0 jour avec la nouvelle adresse d'h\u00f4te"
},
"error": {
"no_key": "Impossible d'obtenir une cl\u00e9 d'API"
},
"step": {
"hassio_confirm": {
"data": {
"allow_clip_sensor": "Autoriser l'importation de capteurs virtuels",
"allow_deconz_groups": "Autoriser l'importation des groupes deCONZ"
},
"description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 la passerelle deCONZ fournie par l'add-on hass.io {addon} ?",
"title": "Passerelle deCONZ Zigbee via l'add-on Hass.io"
},
"init": {
"data": {
"host": "H\u00f4te",

View File

@@ -3,7 +3,8 @@
"abort": {
"already_configured": "Bryggan \u00e4r redan konfigurerad",
"no_bridges": "Inga deCONZ-bryggor uppt\u00e4cktes",
"one_instance_only": "Komponenten st\u00f6djer endast en deCONZ-instans"
"one_instance_only": "Komponenten st\u00f6djer endast en deCONZ-instans",
"updated_instance": "Uppdaterad deCONZ-instans med ny v\u00e4rdadress"
},
"error": {
"no_key": "Det gick inte att ta emot en API-nyckel"
@@ -11,8 +12,10 @@
"step": {
"hassio_confirm": {
"data": {
"allow_clip_sensor": "Till\u00e5t import av virtuella sensorer"
"allow_clip_sensor": "Till\u00e5t import av virtuella sensorer",
"allow_deconz_groups": "Till\u00e5t import av deCONZ-grupper"
},
"description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till deCONZ gateway som tillhandah\u00e5lls av hass.io till\u00e4gg {addon}?",
"title": "deCONZ Zigbee gateway via Hass.io till\u00e4gg"
},
"init": {

View File

@@ -164,6 +164,7 @@ async def async_unload_entry(hass, config_entry):
if not hass.data[DOMAIN]:
hass.services.async_remove(DOMAIN, SERVICE_DECONZ)
hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH)
elif gateway.master:
await async_populate_options(hass, config_entry)
new_master_gateway = next(iter(hass.data[DOMAIN].values()))

View File

@@ -1,6 +1,8 @@
"""Support for deCONZ binary sensors."""
from pydeconz.sensor import Presence, Vibration
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -15,7 +17,7 @@ ATTR_VIBRATIONSTRENGTH = 'vibrationstrength'
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ binary sensors."""
"""Old way of setting up deCONZ platforms."""
pass
@@ -26,12 +28,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
def async_add_sensor(sensors):
"""Add binary sensor from deCONZ."""
from pydeconz.sensor import DECONZ_BINARY_SENSOR
entities = []
for sensor in sensors:
if sensor.type in DECONZ_BINARY_SENSOR and \
if sensor.BINARY and \
not (not gateway.allow_clip_sensor and
sensor.type.startswith('CLIP')):
@@ -49,16 +50,11 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice):
"""Representation of a deCONZ binary sensor."""
@callback
def async_update_callback(self, reason):
"""Update the sensor's state.
If reason is that state is updated,
or reachable has changed or battery has changed.
"""
if reason['state'] or \
'reachable' in reason['attr'] or \
'battery' in reason['attr'] or \
'on' in reason['attr']:
def async_update_callback(self, force_update=False):
"""Update the sensor's state."""
changed = set(self._device.changed_keys)
keys = {'battery', 'on', 'reachable', 'state'}
if force_update or any(key in changed for key in keys):
self.async_schedule_update_ha_state()
@property
@@ -69,26 +65,33 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice):
@property
def device_class(self):
"""Return the class of the sensor."""
return self._device.sensor_class
return self._device.SENSOR_CLASS
@property
def icon(self):
"""Return the icon to use in the frontend."""
return self._device.sensor_icon
return self._device.SENSOR_ICON
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
from pydeconz.sensor import PRESENCE, VIBRATION
attr = {}
if self._device.battery:
attr[ATTR_BATTERY_LEVEL] = self._device.battery
if self._device.on is not None:
attr[ATTR_ON] = self._device.on
if self._device.type in PRESENCE and self._device.dark is not None:
if self._device.secondary_temperature is not None:
attr[ATTR_TEMPERATURE] = self._device.secondary_temperature
if self._device.type in Presence.ZHATYPE and \
self._device.dark is not None:
attr[ATTR_DARK] = self._device.dark
elif self._device.type in VIBRATION:
elif self._device.type in Vibration.ZHATYPE:
attr[ATTR_ORIENTATION] = self._device.orientation
attr[ATTR_TILTANGLE] = self._device.tiltangle
attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibrationstrength
return attr

View File

@@ -1,4 +1,6 @@
"""Support for deCONZ climate devices."""
from pydeconz.sensor import Thermostat
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import (
SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE)
@@ -12,6 +14,12 @@ from .deconz_device import DeconzDevice
from .gateway import get_gateway_from_config_entry
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ platforms."""
pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ climate devices.
@@ -22,12 +30,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
def async_add_climate(sensors):
"""Add climate devices from deCONZ."""
from pydeconz.sensor import THERMOSTAT
entities = []
for sensor in sensors:
if sensor.type in THERMOSTAT and \
if sensor.type in Thermostat.ZHATYPE and \
not (not gateway.allow_clip_sensor and
sensor.type.startswith('CLIP')):
@@ -59,7 +66,7 @@ class DeconzThermostat(DeconzDevice, ClimateDevice):
@property
def is_on(self):
"""Return true if on."""
return self._device.on
return self._device.state_on
async def async_turn_on(self):
"""Turn on switch."""

View File

@@ -4,13 +4,19 @@ import asyncio
import async_timeout
import voluptuous as vol
from pydeconz.errors import ResponseError, RequestError
from pydeconz.utils import (
async_discovery, async_get_api_key, async_get_bridgeid)
from homeassistant import config_entries
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
from .const import CONF_BRIDGEID, DEFAULT_PORT, DOMAIN
DECONZ_MANUFACTURERURL = 'http://www.dresden-elektronik.de'
CONF_SERIAL = 'serial'
@@ -54,8 +60,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
If more than one bridge is found let user choose bridge to link.
If no bridge is found allow user to manually input configuration.
"""
from pydeconz.utils import async_discovery
if user_input is not None:
for bridge in self.bridges:
if bridge[CONF_HOST] == user_input[CONF_HOST]:
@@ -101,8 +105,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
async def async_step_link(self, user_input=None):
"""Attempt to link with the deCONZ bridge."""
from pydeconz.errors import ResponseError, RequestError
from pydeconz.utils import async_get_api_key
errors = {}
if user_input is not None:
@@ -127,8 +129,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
async def _create_entry(self):
"""Create entry for gateway."""
from pydeconz.utils import async_get_bridgeid
if CONF_BRIDGEID not in self.deconz_config:
session = aiohttp_client.async_get_clientsession(self.hass)
@@ -151,12 +151,12 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
entry.data[CONF_HOST] = host
self.hass.config_entries.async_update_entry(entry)
async def async_step_discovery(self, discovery_info):
"""Prepare configuration for a discovered deCONZ bridge.
async def async_step_ssdp(self, discovery_info):
"""Handle a discovered deCONZ bridge."""
if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL:
return self.async_abort(reason='not_deconz_bridge')
This flow is triggered by the discovery component.
"""
bridgeid = discovery_info[CONF_SERIAL]
bridgeid = discovery_info[ATTR_SERIAL]
gateway_entries = configured_gateways(self.hass)
if bridgeid in gateway_entries:
@@ -164,10 +164,17 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
await self._update_entry(entry, discovery_info[CONF_HOST])
return self.async_abort(reason='updated_instance')
# pylint: disable=unsupported-assignment-operation
self.context[ATTR_SERIAL] = bridgeid
if any(bridgeid == flow['context'][ATTR_SERIAL]
for flow in self._async_in_progress()):
return self.async_abort(reason='already_in_progress')
deconz_config = {
CONF_HOST: discovery_info[CONF_HOST],
CONF_PORT: discovery_info[CONF_PORT],
CONF_BRIDGEID: discovery_info[CONF_SERIAL]
CONF_BRIDGEID: bridgeid
}
return await self.async_step_import(deconz_config)

View File

@@ -14,7 +14,7 @@ ZIGBEE_SPEC = ['lumi.curtain']
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Unsupported way of setting up deCONZ covers."""
"""Old way of setting up deCONZ platforms."""
pass

View File

@@ -31,7 +31,7 @@ class DeconzDevice(Entity):
self.unsub_dispatcher()
@callback
def async_update_callback(self, reason):
def async_update_callback(self, force_update=False):
"""Update the device's state."""
self.async_schedule_update_ha_state()

View File

@@ -2,6 +2,9 @@
import asyncio
import async_timeout
from pydeconz import DeconzSession, errors
from pydeconz.sensor import Switch
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.const import CONF_EVENT, CONF_HOST, CONF_ID
from homeassistant.core import EventOrigin, callback
@@ -126,8 +129,7 @@ class DeconzGateway:
def async_connection_status_callback(self, available):
"""Handle signals of gateway connection status."""
self.available = available
async_dispatcher_send(self.hass, self.event_reachable,
{'state': True, 'attr': 'reachable'})
async_dispatcher_send(self.hass, self.event_reachable, True)
@callback
def async_event_new_device(self, device_type):
@@ -145,9 +147,8 @@ class DeconzGateway:
@callback
def async_add_remote(self, sensors):
"""Set up remote from deCONZ."""
from pydeconz.sensor import SWITCH as DECONZ_REMOTE
for sensor in sensors:
if sensor.type in DECONZ_REMOTE and \
if sensor.type in Switch.ZHATYPE and \
not (not self.allow_clip_sensor and
sensor.type.startswith('CLIP')):
self.events.append(DeconzEvent(self.hass, sensor))
@@ -187,8 +188,6 @@ class DeconzGateway:
async def get_gateway(hass, config, async_add_device_callback,
async_connection_status_callback):
"""Create a gateway object and verify configuration."""
from pydeconz import DeconzSession, errors
session = aiohttp_client.async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, session, **config,
@@ -232,8 +231,8 @@ class DeconzEvent:
self._device = None
@callback
def async_update_callback(self, reason):
def async_update_callback(self, force_update=False):
"""Fire the event if reason is that state is updated."""
if reason['state']:
if 'state' in self._device.changed_keys:
data = {CONF_ID: self._id, CONF_EVENT: self._device.state}
self._hass.bus.async_fire(self._event, data, EventOrigin.remote)

View File

@@ -15,7 +15,7 @@ from .gateway import get_gateway_from_config_entry
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ lights and group."""
"""Old way of setting up deCONZ platforms."""
pass

View File

@@ -1,10 +1,16 @@
{
"domain": "deconz",
"name": "Deconz",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/deconz",
"requirements": [
"pydeconz==58"
"pydeconz==59"
],
"ssdp": {
"manufacturer": [
"Royal Philips Electronics"
]
},
"dependencies": [],
"codeowners": [
"@kane610"

View File

@@ -9,7 +9,7 @@ from .gateway import get_gateway_from_config_entry
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ scenes."""
"""Old way of setting up deCONZ platforms."""
pass

View File

@@ -1,6 +1,8 @@
"""Support for deCONZ sensors."""
from pydeconz.sensor import LightLevel, Switch
from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY)
ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import slugify
@@ -16,7 +18,7 @@ ATTR_EVENT_ID = 'event_id'
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ sensors."""
"""Old way of setting up deCONZ platforms."""
pass
@@ -27,17 +29,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
def async_add_sensor(sensors):
"""Add sensors from deCONZ."""
from pydeconz.sensor import (
DECONZ_SENSOR, SWITCH as DECONZ_REMOTE)
entities = []
for sensor in sensors:
if sensor.type in DECONZ_SENSOR and \
if not sensor.BINARY and \
not (not gateway.allow_clip_sensor and
sensor.type.startswith('CLIP')):
if sensor.type in DECONZ_REMOTE:
if sensor.type in Switch.ZHATYPE:
if sensor.battery:
entities.append(DeconzBattery(sensor, gateway))
@@ -56,16 +56,11 @@ class DeconzSensor(DeconzDevice):
"""Representation of a deCONZ sensor."""
@callback
def async_update_callback(self, reason):
"""Update the sensor's state.
If reason is that state is updated,
or reachable has changed or battery has changed.
"""
if reason['state'] or \
'reachable' in reason['attr'] or \
'battery' in reason['attr'] or \
'on' in reason['attr']:
def async_update_callback(self, force_update=False):
"""Update the sensor's state."""
changed = set(self._device.changed_keys)
keys = {'battery', 'on', 'reachable', 'state'}
if force_update or any(key in changed for key in keys):
self.async_schedule_update_ha_state()
@property
@@ -76,34 +71,42 @@ class DeconzSensor(DeconzDevice):
@property
def device_class(self):
"""Return the class of the sensor."""
return self._device.sensor_class
return self._device.SENSOR_CLASS
@property
def icon(self):
"""Return the icon to use in the frontend."""
return self._device.sensor_icon
return self._device.SENSOR_ICON
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this sensor."""
return self._device.sensor_unit
return self._device.SENSOR_UNIT
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
from pydeconz.sensor import LIGHTLEVEL
attr = {}
if self._device.battery:
attr[ATTR_BATTERY_LEVEL] = self._device.battery
if self._device.on is not None:
attr[ATTR_ON] = self._device.on
if self._device.type in LIGHTLEVEL and self._device.dark is not None:
if self._device.secondary_temperature is not None:
attr[ATTR_TEMPERATURE] = self._device.secondary_temperature
if self._device.type in LightLevel.ZHATYPE and \
self._device.dark is not None:
attr[ATTR_DARK] = self._device.dark
if self.unit_of_measurement == 'Watts':
attr[ATTR_CURRENT] = self._device.current
attr[ATTR_VOLTAGE] = self._device.voltage
if self._device.sensor_class == 'daylight':
if self._device.SENSOR_CLASS == 'daylight':
attr[ATTR_DAYLIGHT] = self._device.daylight
return attr
@@ -118,9 +121,11 @@ class DeconzBattery(DeconzDevice):
self._unit_of_measurement = "%"
@callback
def async_update_callback(self, reason):
def async_update_callback(self, force_update=False):
"""Update the battery's state, if needed."""
if 'reachable' in reason['attr'] or 'battery' in reason['attr']:
changed = set(self._device.changed_keys)
keys = {'battery', 'reachable'}
if force_update or any(key in changed for key in keys):
self.async_schedule_update_ha_state()
@property

View File

@@ -34,9 +34,11 @@
},
"abort": {
"already_configured": "Bridge is already configured",
"already_in_progress": "Config flow for bridge is already in progress.",
"no_bridges": "No deCONZ bridges discovered",
"updated_instance": "Updated deCONZ instance with new host address",
"one_instance_only": "Component only supports one deCONZ instance"
"not_deconz_bridge": "Not a deCONZ bridge",
"one_instance_only": "Component only supports one deCONZ instance",
"updated_instance": "Updated deCONZ instance with new host address"
}
}
}

View File

@@ -10,7 +10,7 @@ from .gateway import get_gateway_from_config_entry
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ switches."""
"""Old way of setting up deCONZ platforms."""
pass

View File

@@ -15,6 +15,7 @@
"mobile_app",
"person",
"script",
"ssdp",
"sun",
"system_health",
"updater",

View File

@@ -5,7 +5,8 @@ from homeassistant.components.media_player.const import (
SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY,
SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET)
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP)
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
import homeassistant.util.dt as dt_util
@@ -35,7 +36,7 @@ YOUTUBE_PLAYER_SUPPORT = \
MUSIC_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_CLEAR_PLAYLIST | \
SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | \
SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | SUPPORT_VOLUME_STEP | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_SELECT_SOUND_MODE
@@ -122,6 +123,16 @@ class AbstractDemoPlayer(MediaPlayerDevice):
self._volume_muted = mute
self.schedule_update_ha_state()
def volume_up(self):
"""Increase volume."""
self._volume_level = min(1.0, self._volume_level + 0.1)
self.schedule_update_ha_state()
def volume_down(self):
"""Decrease volume."""
self._volume_level = max(0.0, self._volume_level - 0.1)
self.schedule_update_ha_state()
def set_volume_level(self, volume):
"""Set the volume level, range 0..1."""
self._volume_level = volume

View File

@@ -1,78 +1,52 @@
"""Provide functionality to keep track of devices."""
import asyncio
from datetime import timedelta
import logging
from typing import Any, List, Sequence, Callable
import voluptuous as vol
from homeassistant.setup import async_prepare_setup_platform
from homeassistant.core import callback
from homeassistant.loader import bind_hass
from homeassistant.components import group, zone
from homeassistant.components.group import (
ATTR_ADD_ENTITIES, ATTR_ENTITIES, ATTR_OBJECT_ID, ATTR_VISIBLE,
DOMAIN as DOMAIN_GROUP, SERVICE_SET)
from homeassistant.components.zone.zone import async_active_zone
from homeassistant.config import load_yaml_config_file, async_log_exception
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform, discovery
from homeassistant.components import group
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
from homeassistant import util
from homeassistant.util.async_ import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util
from homeassistant.util.yaml import dump
from homeassistant.helpers.event import async_track_utc_time_change
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_GPS_ACCURACY, ATTR_ICON, ATTR_LATITUDE,
ATTR_LONGITUDE, ATTR_NAME, CONF_ICON, CONF_MAC, CONF_NAME,
DEVICE_DEFAULT_NAME, STATE_NOT_HOME, STATE_HOME)
from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME
_LOGGER = logging.getLogger(__name__)
from . import legacy, setup
from .config_entry import ( # noqa # pylint: disable=unused-import
async_setup_entry, async_unload_entry
)
from .legacy import DeviceScanner # noqa # pylint: disable=unused-import
from .const import (
ATTR_ATTRIBUTES,
ATTR_BATTERY,
ATTR_CONSIDER_HOME,
ATTR_DEV_ID,
ATTR_GPS,
ATTR_HOST_NAME,
ATTR_LOCATION_NAME,
ATTR_MAC,
ATTR_SOURCE_TYPE,
CONF_AWAY_HIDE,
CONF_CONSIDER_HOME,
CONF_NEW_DEVICE_DEFAULTS,
CONF_SCAN_INTERVAL,
CONF_TRACK_NEW,
DEFAULT_AWAY_HIDE,
DEFAULT_CONSIDER_HOME,
DEFAULT_TRACK_NEW,
DOMAIN,
PLATFORM_TYPE_LEGACY,
SOURCE_TYPE_BLUETOOTH_LE,
SOURCE_TYPE_BLUETOOTH,
SOURCE_TYPE_GPS,
SOURCE_TYPE_ROUTER,
)
DOMAIN = 'device_tracker'
GROUP_NAME_ALL_DEVICES = 'all devices'
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices')
ENTITY_ID_FORMAT = DOMAIN + '.{}'
YAML_DEVICES = 'known_devices.yaml'
CONF_TRACK_NEW = 'track_new_devices'
DEFAULT_TRACK_NEW = True
CONF_NEW_DEVICE_DEFAULTS = 'new_device_defaults'
CONF_CONSIDER_HOME = 'consider_home'
DEFAULT_CONSIDER_HOME = timedelta(seconds=180)
CONF_SCAN_INTERVAL = 'interval_seconds'
DEFAULT_SCAN_INTERVAL = timedelta(seconds=12)
CONF_AWAY_HIDE = 'hide_if_away'
DEFAULT_AWAY_HIDE = False
EVENT_NEW_DEVICE = 'device_tracker_new_device'
SERVICE_SEE = 'see'
ATTR_ATTRIBUTES = 'attributes'
ATTR_BATTERY = 'battery'
ATTR_DEV_ID = 'dev_id'
ATTR_GPS = 'gps'
ATTR_HOST_NAME = 'host_name'
ATTR_LOCATION_NAME = 'location_name'
ATTR_MAC = 'mac'
ATTR_SOURCE_TYPE = 'source_type'
ATTR_CONSIDER_HOME = 'consider_home'
SOURCE_TYPE_GPS = 'gps'
SOURCE_TYPE_ROUTER = 'router'
SOURCE_TYPE_BLUETOOTH = 'bluetooth'
SOURCE_TYPE_BLUETOOTH_LE = 'bluetooth_le'
SOURCE_TYPES = (SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER,
SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH_LE)
@@ -136,75 +110,29 @@ def see(hass: HomeAssistantType, mac: str = None, dev_id: str = None,
async def async_setup(hass: HomeAssistantType, config: ConfigType):
"""Set up the device tracker."""
yaml_path = hass.config.path(YAML_DEVICES)
tracker = await legacy.get_tracker(hass, config)
conf = config.get(DOMAIN, [])
conf = conf[0] if conf else {}
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
legacy_platforms = await setup.async_extract_config(hass, config)
defaults = conf.get(CONF_NEW_DEVICE_DEFAULTS, {})
track_new = conf.get(CONF_TRACK_NEW)
if track_new is None:
track_new = defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
setup_tasks = [
legacy_platform.async_setup_legacy(hass, tracker)
for legacy_platform in legacy_platforms
]
devices = await async_load_config(yaml_path, hass, consider_home)
tracker = DeviceTracker(
hass, consider_home, track_new, defaults, devices)
async def async_setup_platform(p_type, p_config, disc_info=None):
"""Set up a device tracker platform."""
platform = await async_prepare_setup_platform(
hass, config, DOMAIN, p_type)
if platform is None:
return
_LOGGER.info("Setting up %s.%s", DOMAIN, p_type)
try:
scanner = None
setup = None
if hasattr(platform, 'async_get_scanner'):
scanner = await platform.async_get_scanner(
hass, {DOMAIN: p_config})
elif hasattr(platform, 'get_scanner'):
scanner = await hass.async_add_job(
platform.get_scanner, hass, {DOMAIN: p_config})
elif hasattr(platform, 'async_setup_scanner'):
setup = await platform.async_setup_scanner(
hass, p_config, tracker.async_see, disc_info)
elif hasattr(platform, 'setup_scanner'):
setup = await hass.async_add_job(
platform.setup_scanner, hass, p_config, tracker.see,
disc_info)
elif hasattr(platform, 'async_setup_entry'):
setup = await platform.async_setup_entry(
hass, p_config, tracker.async_see)
else:
raise HomeAssistantError("Invalid device_tracker platform.")
if scanner:
async_setup_scanner_platform(
hass, p_config, scanner, tracker.async_see, p_type)
return
if not setup:
_LOGGER.error("Error setting up platform %s", p_type)
return
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error setting up platform %s", p_type)
hass.data[DOMAIN] = async_setup_platform
setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config
in config_per_platform(config, DOMAIN)]
if setup_tasks:
await asyncio.wait(setup_tasks, loop=hass.loop)
await asyncio.wait(setup_tasks)
tracker.async_setup_group()
async def async_platform_discovered(platform, info):
async def async_platform_discovered(p_type, info):
"""Load a platform."""
await async_setup_platform(platform, {}, disc_info=info)
platform = await setup.async_create_platform_type(
hass, config, p_type, {})
if platform is None or platform.type != PLATFORM_TYPE_LEGACY:
return
await platform.async_setup_legacy(hass, tracker, info)
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
@@ -226,537 +154,3 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
# restore
await tracker.async_setup_tracked_device()
return True
async def async_setup_entry(hass, entry):
"""Set up an entry."""
await hass.data[DOMAIN](entry.domain, entry)
return True
class DeviceTracker:
"""Representation of a device tracker."""
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track_new: bool, defaults: dict,
devices: Sequence) -> None:
"""Initialize a device tracker."""
self.hass = hass
self.devices = {dev.dev_id: dev for dev in devices}
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
self.consider_home = consider_home
self.track_new = track_new if track_new is not None \
else defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
self.defaults = defaults
self.group = None
self._is_updating = asyncio.Lock(loop=hass.loop)
for dev in devices:
if self.devices[dev.dev_id] is not dev:
_LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id)
if dev.mac and self.mac_to_dev[dev.mac] is not dev:
_LOGGER.warning('Duplicate device MAC addresses detected %s',
dev.mac)
def see(self, mac: str = None, dev_id: str = None, host_name: str = None,
location_name: str = None, gps: GPSType = None,
gps_accuracy: int = None, battery: int = None,
attributes: dict = None, source_type: str = SOURCE_TYPE_GPS,
picture: str = None, icon: str = None,
consider_home: timedelta = None):
"""Notify the device tracker that you see a device."""
self.hass.add_job(
self.async_see(mac, dev_id, host_name, location_name, gps,
gps_accuracy, battery, attributes, source_type,
picture, icon, consider_home)
)
async def async_see(
self, mac: str = None, dev_id: str = None, host_name: str = None,
location_name: str = None, gps: GPSType = None,
gps_accuracy: int = None, battery: int = None,
attributes: dict = None, source_type: str = SOURCE_TYPE_GPS,
picture: str = None, icon: str = None,
consider_home: timedelta = None):
"""Notify the device tracker that you see a device.
This method is a coroutine.
"""
if mac is None and dev_id is None:
raise HomeAssistantError('Neither mac or device id passed in')
if mac is not None:
mac = str(mac).upper()
device = self.mac_to_dev.get(mac)
if not device:
dev_id = util.slugify(host_name or '') or util.slugify(mac)
else:
dev_id = cv.slug(str(dev_id).lower())
device = self.devices.get(dev_id)
if device:
await device.async_seen(
host_name, location_name, gps, gps_accuracy, battery,
attributes, source_type, consider_home)
if device.track:
await device.async_update_ha_state()
return
# If no device can be found, create it
dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
device = Device(
self.hass, consider_home or self.consider_home, self.track_new,
dev_id, mac, (host_name or dev_id).replace('_', ' '),
picture=picture, icon=icon,
hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
self.devices[dev_id] = device
if mac is not None:
self.mac_to_dev[mac] = device
await device.async_seen(
host_name, location_name, gps, gps_accuracy, battery, attributes,
source_type)
if device.track:
await device.async_update_ha_state()
# During init, we ignore the group
if self.group and self.track_new:
self.hass.async_create_task(
self.hass.async_call(
DOMAIN_GROUP, SERVICE_SET, {
ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES),
ATTR_VISIBLE: False,
ATTR_NAME: GROUP_NAME_ALL_DEVICES,
ATTR_ADD_ENTITIES: [device.entity_id]}))
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
ATTR_ENTITY_ID: device.entity_id,
ATTR_HOST_NAME: device.host_name,
ATTR_MAC: device.mac,
})
# update known_devices.yaml
self.hass.async_create_task(
self.async_update_config(
self.hass.config.path(YAML_DEVICES), dev_id, device)
)
async def async_update_config(self, path, dev_id, device):
"""Add device to YAML configuration file.
This method is a coroutine.
"""
async with self._is_updating:
await self.hass.async_add_executor_job(
update_config, self.hass.config.path(YAML_DEVICES),
dev_id, device)
@callback
def async_setup_group(self):
"""Initialize group for all tracked devices.
This method must be run in the event loop.
"""
entity_ids = [dev.entity_id for dev in self.devices.values()
if dev.track]
self.hass.async_create_task(
self.hass.services.async_call(
DOMAIN_GROUP, SERVICE_SET, {
ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES),
ATTR_VISIBLE: False,
ATTR_NAME: GROUP_NAME_ALL_DEVICES,
ATTR_ENTITIES: entity_ids}))
@callback
def async_update_stale(self, now: dt_util.dt.datetime):
"""Update stale devices.
This method must be run in the event loop.
"""
for device in self.devices.values():
if (device.track and device.last_update_home) and \
device.stale(now):
self.hass.async_create_task(device.async_update_ha_state(True))
async def async_setup_tracked_device(self):
"""Set up all not exists tracked devices.
This method is a coroutine.
"""
async def async_init_single_device(dev):
"""Init a single device_tracker entity."""
await dev.async_added_to_hass()
await dev.async_update_ha_state()
tasks = []
for device in self.devices.values():
if device.track and not device.last_seen:
tasks.append(self.hass.async_create_task(
async_init_single_device(device)))
if tasks:
await asyncio.wait(tasks, loop=self.hass.loop)
class Device(RestoreEntity):
"""Represent a tracked device."""
host_name = None # type: str
location_name = None # type: str
gps = None # type: GPSType
gps_accuracy = 0 # type: int
last_seen = None # type: dt_util.dt.datetime
consider_home = None # type: dt_util.dt.timedelta
battery = None # type: int
attributes = None # type: dict
icon = None # type: str
# Track if the last update of this device was HOME.
last_update_home = False
_state = STATE_NOT_HOME
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track: bool, dev_id: str, mac: str, name: str = None,
picture: str = None, gravatar: str = None, icon: str = None,
hide_if_away: bool = False) -> None:
"""Initialize a device."""
self.hass = hass
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
# Timedelta object how long we consider a device home if it is not
# detected anymore.
self.consider_home = consider_home
# Device ID
self.dev_id = dev_id
self.mac = mac
# If we should track this device
self.track = track
# Configured name
self.config_name = name
# Configured picture
if gravatar is not None:
self.config_picture = get_gravatar_for_email(gravatar)
else:
self.config_picture = picture
self.icon = icon
self.away_hide = hide_if_away
self.source_type = None
self._attributes = {}
@property
def name(self):
"""Return the name of the entity."""
return self.config_name or self.host_name or DEVICE_DEFAULT_NAME
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def entity_picture(self):
"""Return the picture of the device."""
return self.config_picture
@property
def state_attributes(self):
"""Return the device state attributes."""
attr = {
ATTR_SOURCE_TYPE: self.source_type
}
if self.gps:
attr[ATTR_LATITUDE] = self.gps[0]
attr[ATTR_LONGITUDE] = self.gps[1]
attr[ATTR_GPS_ACCURACY] = self.gps_accuracy
if self.battery:
attr[ATTR_BATTERY] = self.battery
return attr
@property
def device_state_attributes(self):
"""Return device state attributes."""
return self._attributes
@property
def hidden(self):
"""If device should be hidden."""
return self.away_hide and self.state != STATE_HOME
async def async_seen(
self, host_name: str = None, location_name: str = None,
gps: GPSType = None, gps_accuracy=0, battery: int = None,
attributes: dict = None,
source_type: str = SOURCE_TYPE_GPS,
consider_home: timedelta = None):
"""Mark the device as seen."""
self.source_type = source_type
self.last_seen = dt_util.utcnow()
self.host_name = host_name
self.location_name = location_name
self.consider_home = consider_home or self.consider_home
if battery:
self.battery = battery
if attributes:
self._attributes.update(attributes)
self.gps = None
if gps is not None:
try:
self.gps = float(gps[0]), float(gps[1])
self.gps_accuracy = gps_accuracy or 0
except (ValueError, TypeError, IndexError):
self.gps = None
self.gps_accuracy = 0
_LOGGER.warning(
"Could not parse gps value for %s: %s", self.dev_id, gps)
# pylint: disable=not-an-iterable
await self.async_update()
def stale(self, now: dt_util.dt.datetime = None):
"""Return if device state is stale.
Async friendly.
"""
return self.last_seen is None or \
(now or dt_util.utcnow()) - self.last_seen > self.consider_home
def mark_stale(self):
"""Mark the device state as stale."""
self._state = STATE_NOT_HOME
self.gps = None
self.last_update_home = False
async def async_update(self):
"""Update state of entity.
This method is a coroutine.
"""
if not self.last_seen:
return
if self.location_name:
self._state = self.location_name
elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS:
zone_state = async_active_zone(
self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
if zone_state is None:
self._state = STATE_NOT_HOME
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
self._state = STATE_HOME
else:
self._state = zone_state.name
elif self.stale():
self.mark_stale()
else:
self._state = STATE_HOME
self.last_update_home = True
async def async_added_to_hass(self):
"""Add an entity."""
await super().async_added_to_hass()
state = await self.async_get_last_state()
if not state:
return
self._state = state.state
self.last_update_home = (state.state == STATE_HOME)
self.last_seen = dt_util.utcnow()
for attr, var in (
(ATTR_SOURCE_TYPE, 'source_type'),
(ATTR_GPS_ACCURACY, 'gps_accuracy'),
(ATTR_BATTERY, 'battery'),
):
if attr in state.attributes:
setattr(self, var, state.attributes[attr])
if ATTR_LONGITUDE in state.attributes:
self.gps = (state.attributes[ATTR_LATITUDE],
state.attributes[ATTR_LONGITUDE])
class DeviceScanner:
"""Device scanner object."""
hass = None # type: HomeAssistantType
def scan_devices(self) -> List[str]:
"""Scan for devices."""
raise NotImplementedError()
def async_scan_devices(self) -> Any:
"""Scan for devices.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.scan_devices)
def get_device_name(self, device: str) -> str:
"""Get the name of a device."""
raise NotImplementedError()
def async_get_device_name(self, device: str) -> Any:
"""Get the name of a device.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.get_device_name, device)
def get_extra_attributes(self, device: str) -> dict:
"""Get the extra attributes of a device."""
raise NotImplementedError()
def async_get_extra_attributes(self, device: str) -> Any:
"""Get the extra attributes of a device.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.get_extra_attributes, device)
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
"""Load devices from YAML configuration file."""
return run_coroutine_threadsafe(
async_load_config(path, hass, consider_home), hass.loop).result()
async def async_load_config(path: str, hass: HomeAssistantType,
consider_home: timedelta):
"""Load devices from YAML configuration file.
This method is a coroutine.
"""
dev_schema = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_ICON, default=None): vol.Any(None, cv.icon),
vol.Optional('track', default=False): cv.boolean,
vol.Optional(CONF_MAC, default=None):
vol.Any(None, vol.All(cv.string, vol.Upper)),
vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean,
vol.Optional('gravatar', default=None): vol.Any(None, cv.string),
vol.Optional('picture', default=None): vol.Any(None, cv.string),
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
cv.time_period, cv.positive_timedelta),
})
try:
result = []
try:
devices = await hass.async_add_job(
load_yaml_config_file, path)
except HomeAssistantError as err:
_LOGGER.error("Unable to load %s: %s", path, str(err))
return []
for dev_id, device in devices.items():
# Deprecated option. We just ignore it to avoid breaking change
device.pop('vendor', None)
try:
device = dev_schema(device)
device['dev_id'] = cv.slugify(dev_id)
except vol.Invalid as exp:
async_log_exception(exp, dev_id, devices, hass)
else:
result.append(Device(hass, **device))
return result
except (HomeAssistantError, FileNotFoundError):
# When YAML file could not be loaded/did not contain a dict
return []
@callback
def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
scanner: Any, async_see_device: Callable,
platform: str):
"""Set up the connect scanner-based platform to device tracker.
This method must be run in the event loop.
"""
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
update_lock = asyncio.Lock(loop=hass.loop)
scanner.hass = hass
# Initial scan of each mac we also tell about host name for config
seen = set() # type: Any
async def async_device_tracker_scan(now: dt_util.dt.datetime):
"""Handle interval matches."""
if update_lock.locked():
_LOGGER.warning(
"Updating device list from %s took longer than the scheduled "
"scan interval %s", platform, interval)
return
async with update_lock:
found_devices = await scanner.async_scan_devices()
for mac in found_devices:
if mac in seen:
host_name = None
else:
host_name = await scanner.async_get_device_name(mac)
seen.add(mac)
try:
extra_attributes = \
await scanner.async_get_extra_attributes(mac)
except NotImplementedError:
extra_attributes = dict()
kwargs = {
'mac': mac,
'host_name': host_name,
'source_type': SOURCE_TYPE_ROUTER,
'attributes': {
'scanner': scanner.__class__.__name__,
**extra_attributes
}
}
zone_home = hass.states.get(zone.ENTITY_ID_HOME)
if zone_home:
kwargs['gps'] = [zone_home.attributes[ATTR_LATITUDE],
zone_home.attributes[ATTR_LONGITUDE]]
kwargs['gps_accuracy'] = 0
hass.async_create_task(async_see_device(**kwargs))
async_track_time_interval(hass, async_device_tracker_scan, interval)
hass.async_create_task(async_device_tracker_scan(None))
def update_config(path: str, dev_id: str, device: Device):
"""Add device to YAML configuration file."""
with open(path, 'a') as out:
device = {device.dev_id: {
ATTR_NAME: device.name,
ATTR_MAC: device.mac,
ATTR_ICON: device.icon,
'picture': device.config_picture,
'track': device.track,
CONF_AWAY_HIDE: device.away_hide,
}}
out.write('\n')
out.write(dump(device))
def get_gravatar_for_email(email: str):
"""Return an 80px Gravatar for the given email address.
Async friendly.
"""
import hashlib
url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar'
return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest())

View File

@@ -0,0 +1,114 @@
"""Code to set up a device tracker platform using a config entry."""
from typing import Optional
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.const import (
STATE_NOT_HOME,
STATE_HOME,
ATTR_GPS_ACCURACY,
ATTR_LATITUDE,
ATTR_LONGITUDE,
ATTR_BATTERY_LEVEL,
)
from homeassistant.components import zone
from .const import (
ATTR_SOURCE_TYPE,
DOMAIN,
LOGGER,
)
async def async_setup_entry(hass, entry):
"""Set up an entry."""
component = hass.data.get(DOMAIN) # type: Optional[EntityComponent]
if component is None:
component = hass.data[DOMAIN] = EntityComponent(
LOGGER, DOMAIN, hass
)
return await component.async_setup_entry(entry)
async def async_unload_entry(hass, entry):
"""Unload an entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)
class DeviceTrackerEntity(Entity):
"""Represent a tracked device."""
@property
def battery_level(self):
"""Return the battery level of the device.
Percentage from 0-100.
"""
return None
@property
def location_accuracy(self):
"""Return the location accuracy of the device.
Value in meters.
"""
return 0
@property
def location_name(self) -> str:
"""Return a location name for the current location of the device."""
return None
@property
def latitude(self) -> float:
"""Return latitude value of the device."""
return NotImplementedError
@property
def longitude(self) -> float:
"""Return longitude value of the device."""
return NotImplementedError
@property
def source_type(self):
"""Return the source type, eg gps or router, of the device."""
raise NotImplementedError
@property
def state(self):
"""Return the state of the device."""
if self.location_name:
return self.location_name
if self.latitude is not None:
zone_state = zone.async_active_zone(
self.hass, self.latitude, self.longitude,
self.location_accuracy)
if zone_state is None:
state = STATE_NOT_HOME
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
state = STATE_HOME
else:
state = zone_state.name
return state
return None
@property
def state_attributes(self):
"""Return the device state attributes."""
attr = {
ATTR_SOURCE_TYPE: self.source_type
}
if self.latitude is not None:
attr[ATTR_LATITUDE] = self.latitude
attr[ATTR_LONGITUDE] = self.longitude
attr[ATTR_GPS_ACCURACY] = self.location_accuracy
if self.battery_level:
attr[ATTR_BATTERY_LEVEL] = self.battery_level
return attr

View File

@@ -0,0 +1,40 @@
"""Device tracker constants."""
from datetime import timedelta
import logging
LOGGER = logging.getLogger(__package__)
DOMAIN = 'device_tracker'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
PLATFORM_TYPE_LEGACY = 'legacy'
PLATFORM_TYPE_ENTITY = 'entity_platform'
SOURCE_TYPE_GPS = 'gps'
SOURCE_TYPE_ROUTER = 'router'
SOURCE_TYPE_BLUETOOTH = 'bluetooth'
SOURCE_TYPE_BLUETOOTH_LE = 'bluetooth_le'
CONF_SCAN_INTERVAL = 'interval_seconds'
SCAN_INTERVAL = timedelta(seconds=12)
CONF_TRACK_NEW = 'track_new_devices'
DEFAULT_TRACK_NEW = True
CONF_AWAY_HIDE = 'hide_if_away'
DEFAULT_AWAY_HIDE = False
CONF_CONSIDER_HOME = 'consider_home'
DEFAULT_CONSIDER_HOME = timedelta(seconds=180)
CONF_NEW_DEVICE_DEFAULTS = 'new_device_defaults'
ATTR_ATTRIBUTES = 'attributes'
ATTR_BATTERY = 'battery'
ATTR_DEV_ID = 'dev_id'
ATTR_GPS = 'gps'
ATTR_HOST_NAME = 'host_name'
ATTR_LOCATION_NAME = 'location_name'
ATTR_MAC = 'mac'
ATTR_SOURCE_TYPE = 'source_type'
ATTR_CONSIDER_HOME = 'consider_home'

View File

@@ -0,0 +1,526 @@
"""Legacy device tracker classes."""
import asyncio
from datetime import timedelta
from typing import Any, List, Sequence
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components import zone
from homeassistant.components.group import (
ATTR_ADD_ENTITIES, ATTR_ENTITIES, ATTR_OBJECT_ID, ATTR_VISIBLE,
DOMAIN as DOMAIN_GROUP, SERVICE_SET)
from homeassistant.components.zone import async_active_zone
from homeassistant.config import load_yaml_config_file, async_log_exception
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import GPSType, HomeAssistantType
from homeassistant import util
import homeassistant.util.dt as dt_util
from homeassistant.util.yaml import dump
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_GPS_ACCURACY, ATTR_ICON, ATTR_LATITUDE,
ATTR_LONGITUDE, ATTR_NAME, CONF_ICON, CONF_MAC, CONF_NAME,
DEVICE_DEFAULT_NAME, STATE_NOT_HOME, STATE_HOME)
from .const import (
ATTR_BATTERY,
ATTR_HOST_NAME,
ATTR_MAC,
ATTR_SOURCE_TYPE,
CONF_AWAY_HIDE,
CONF_CONSIDER_HOME,
CONF_NEW_DEVICE_DEFAULTS,
CONF_TRACK_NEW,
DEFAULT_AWAY_HIDE,
DEFAULT_CONSIDER_HOME,
DEFAULT_TRACK_NEW,
DOMAIN,
ENTITY_ID_FORMAT,
LOGGER,
SOURCE_TYPE_GPS,
)
YAML_DEVICES = 'known_devices.yaml'
GROUP_NAME_ALL_DEVICES = 'all devices'
EVENT_NEW_DEVICE = 'device_tracker_new_device'
async def get_tracker(hass, config):
"""Create a tracker."""
yaml_path = hass.config.path(YAML_DEVICES)
conf = config.get(DOMAIN, [])
conf = conf[0] if conf else {}
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
defaults = conf.get(CONF_NEW_DEVICE_DEFAULTS, {})
track_new = conf.get(CONF_TRACK_NEW)
if track_new is None:
track_new = defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
devices = await async_load_config(yaml_path, hass, consider_home)
tracker = DeviceTracker(
hass, consider_home, track_new, defaults, devices)
return tracker
class DeviceTracker:
"""Representation of a device tracker."""
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track_new: bool, defaults: dict,
devices: Sequence) -> None:
"""Initialize a device tracker."""
self.hass = hass
self.devices = {dev.dev_id: dev for dev in devices}
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
self.consider_home = consider_home
self.track_new = track_new if track_new is not None \
else defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
self.defaults = defaults
self.group = None
self._is_updating = asyncio.Lock()
for dev in devices:
if self.devices[dev.dev_id] is not dev:
LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id)
if dev.mac and self.mac_to_dev[dev.mac] is not dev:
LOGGER.warning('Duplicate device MAC addresses detected %s',
dev.mac)
def see(self, mac: str = None, dev_id: str = None, host_name: str = None,
location_name: str = None, gps: GPSType = None,
gps_accuracy: int = None, battery: int = None,
attributes: dict = None, source_type: str = SOURCE_TYPE_GPS,
picture: str = None, icon: str = None,
consider_home: timedelta = None):
"""Notify the device tracker that you see a device."""
self.hass.add_job(
self.async_see(mac, dev_id, host_name, location_name, gps,
gps_accuracy, battery, attributes, source_type,
picture, icon, consider_home)
)
async def async_see(
self, mac: str = None, dev_id: str = None, host_name: str = None,
location_name: str = None, gps: GPSType = None,
gps_accuracy: int = None, battery: int = None,
attributes: dict = None, source_type: str = SOURCE_TYPE_GPS,
picture: str = None, icon: str = None,
consider_home: timedelta = None):
"""Notify the device tracker that you see a device.
This method is a coroutine.
"""
if mac is None and dev_id is None:
raise HomeAssistantError('Neither mac or device id passed in')
if mac is not None:
mac = str(mac).upper()
device = self.mac_to_dev.get(mac)
if not device:
dev_id = util.slugify(host_name or '') or util.slugify(mac)
else:
dev_id = cv.slug(str(dev_id).lower())
device = self.devices.get(dev_id)
if device:
await device.async_seen(
host_name, location_name, gps, gps_accuracy, battery,
attributes, source_type, consider_home)
if device.track:
await device.async_update_ha_state()
return
# If no device can be found, create it
dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
device = Device(
self.hass, consider_home or self.consider_home, self.track_new,
dev_id, mac, (host_name or dev_id).replace('_', ' '),
picture=picture, icon=icon,
hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
self.devices[dev_id] = device
if mac is not None:
self.mac_to_dev[mac] = device
await device.async_seen(
host_name, location_name, gps, gps_accuracy, battery, attributes,
source_type)
if device.track:
await device.async_update_ha_state()
# During init, we ignore the group
if self.group and self.track_new:
self.hass.async_create_task(
self.hass.async_call(
DOMAIN_GROUP, SERVICE_SET, {
ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES),
ATTR_VISIBLE: False,
ATTR_NAME: GROUP_NAME_ALL_DEVICES,
ATTR_ADD_ENTITIES: [device.entity_id]}))
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
ATTR_ENTITY_ID: device.entity_id,
ATTR_HOST_NAME: device.host_name,
ATTR_MAC: device.mac,
})
# update known_devices.yaml
self.hass.async_create_task(
self.async_update_config(
self.hass.config.path(YAML_DEVICES), dev_id, device)
)
async def async_update_config(self, path, dev_id, device):
"""Add device to YAML configuration file.
This method is a coroutine.
"""
async with self._is_updating:
await self.hass.async_add_executor_job(
update_config, self.hass.config.path(YAML_DEVICES),
dev_id, device)
@callback
def async_setup_group(self):
"""Initialize group for all tracked devices.
This method must be run in the event loop.
"""
entity_ids = [dev.entity_id for dev in self.devices.values()
if dev.track]
self.hass.async_create_task(
self.hass.services.async_call(
DOMAIN_GROUP, SERVICE_SET, {
ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES),
ATTR_VISIBLE: False,
ATTR_NAME: GROUP_NAME_ALL_DEVICES,
ATTR_ENTITIES: entity_ids}))
@callback
def async_update_stale(self, now: dt_util.dt.datetime):
"""Update stale devices.
This method must be run in the event loop.
"""
for device in self.devices.values():
if (device.track and device.last_update_home) and \
device.stale(now):
self.hass.async_create_task(device.async_update_ha_state(True))
async def async_setup_tracked_device(self):
"""Set up all not exists tracked devices.
This method is a coroutine.
"""
async def async_init_single_device(dev):
"""Init a single device_tracker entity."""
await dev.async_added_to_hass()
await dev.async_update_ha_state()
tasks = []
for device in self.devices.values():
if device.track and not device.last_seen:
tasks.append(self.hass.async_create_task(
async_init_single_device(device)))
if tasks:
await asyncio.wait(tasks)
class Device(RestoreEntity):
"""Represent a tracked device."""
host_name = None # type: str
location_name = None # type: str
gps = None # type: GPSType
gps_accuracy = 0 # type: int
last_seen = None # type: dt_util.dt.datetime
consider_home = None # type: dt_util.dt.timedelta
battery = None # type: int
attributes = None # type: dict
icon = None # type: str
# Track if the last update of this device was HOME.
last_update_home = False
_state = STATE_NOT_HOME
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track: bool, dev_id: str, mac: str, name: str = None,
picture: str = None, gravatar: str = None, icon: str = None,
hide_if_away: bool = False) -> None:
"""Initialize a device."""
self.hass = hass
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
# Timedelta object how long we consider a device home if it is not
# detected anymore.
self.consider_home = consider_home
# Device ID
self.dev_id = dev_id
self.mac = mac
# If we should track this device
self.track = track
# Configured name
self.config_name = name
# Configured picture
if gravatar is not None:
self.config_picture = get_gravatar_for_email(gravatar)
else:
self.config_picture = picture
self.icon = icon
self.away_hide = hide_if_away
self.source_type = None
self._attributes = {}
@property
def name(self):
"""Return the name of the entity."""
return self.config_name or self.host_name or DEVICE_DEFAULT_NAME
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def entity_picture(self):
"""Return the picture of the device."""
return self.config_picture
@property
def state_attributes(self):
"""Return the device state attributes."""
attr = {
ATTR_SOURCE_TYPE: self.source_type
}
if self.gps:
attr[ATTR_LATITUDE] = self.gps[0]
attr[ATTR_LONGITUDE] = self.gps[1]
attr[ATTR_GPS_ACCURACY] = self.gps_accuracy
if self.battery:
attr[ATTR_BATTERY] = self.battery
return attr
@property
def device_state_attributes(self):
"""Return device state attributes."""
return self._attributes
@property
def hidden(self):
"""If device should be hidden."""
return self.away_hide and self.state != STATE_HOME
async def async_seen(
self, host_name: str = None, location_name: str = None,
gps: GPSType = None, gps_accuracy=0, battery: int = None,
attributes: dict = None,
source_type: str = SOURCE_TYPE_GPS,
consider_home: timedelta = None):
"""Mark the device as seen."""
self.source_type = source_type
self.last_seen = dt_util.utcnow()
self.host_name = host_name
self.location_name = location_name
self.consider_home = consider_home or self.consider_home
if battery:
self.battery = battery
if attributes:
self._attributes.update(attributes)
self.gps = None
if gps is not None:
try:
self.gps = float(gps[0]), float(gps[1])
self.gps_accuracy = gps_accuracy or 0
except (ValueError, TypeError, IndexError):
self.gps = None
self.gps_accuracy = 0
LOGGER.warning(
"Could not parse gps value for %s: %s", self.dev_id, gps)
# pylint: disable=not-an-iterable
await self.async_update()
def stale(self, now: dt_util.dt.datetime = None):
"""Return if device state is stale.
Async friendly.
"""
return self.last_seen is None or \
(now or dt_util.utcnow()) - self.last_seen > self.consider_home
def mark_stale(self):
"""Mark the device state as stale."""
self._state = STATE_NOT_HOME
self.gps = None
self.last_update_home = False
async def async_update(self):
"""Update state of entity.
This method is a coroutine.
"""
if not self.last_seen:
return
if self.location_name:
self._state = self.location_name
elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS:
zone_state = async_active_zone(
self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
if zone_state is None:
self._state = STATE_NOT_HOME
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
self._state = STATE_HOME
else:
self._state = zone_state.name
elif self.stale():
self.mark_stale()
else:
self._state = STATE_HOME
self.last_update_home = True
async def async_added_to_hass(self):
"""Add an entity."""
await super().async_added_to_hass()
state = await self.async_get_last_state()
if not state:
return
self._state = state.state
self.last_update_home = (state.state == STATE_HOME)
self.last_seen = dt_util.utcnow()
for attr, var in (
(ATTR_SOURCE_TYPE, 'source_type'),
(ATTR_GPS_ACCURACY, 'gps_accuracy'),
(ATTR_BATTERY, 'battery'),
):
if attr in state.attributes:
setattr(self, var, state.attributes[attr])
if ATTR_LONGITUDE in state.attributes:
self.gps = (state.attributes[ATTR_LATITUDE],
state.attributes[ATTR_LONGITUDE])
class DeviceScanner:
"""Device scanner object."""
hass = None # type: HomeAssistantType
def scan_devices(self) -> List[str]:
"""Scan for devices."""
raise NotImplementedError()
def async_scan_devices(self) -> Any:
"""Scan for devices.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.scan_devices)
def get_device_name(self, device: str) -> str:
"""Get the name of a device."""
raise NotImplementedError()
def async_get_device_name(self, device: str) -> Any:
"""Get the name of a device.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.get_device_name, device)
def get_extra_attributes(self, device: str) -> dict:
"""Get the extra attributes of a device."""
raise NotImplementedError()
def async_get_extra_attributes(self, device: str) -> Any:
"""Get the extra attributes of a device.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.get_extra_attributes, device)
async def async_load_config(path: str, hass: HomeAssistantType,
consider_home: timedelta):
"""Load devices from YAML configuration file.
This method is a coroutine.
"""
dev_schema = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_ICON, default=None): vol.Any(None, cv.icon),
vol.Optional('track', default=False): cv.boolean,
vol.Optional(CONF_MAC, default=None):
vol.Any(None, vol.All(cv.string, vol.Upper)),
vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean,
vol.Optional('gravatar', default=None): vol.Any(None, cv.string),
vol.Optional('picture', default=None): vol.Any(None, cv.string),
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
cv.time_period, cv.positive_timedelta),
})
result = []
try:
devices = await hass.async_add_job(
load_yaml_config_file, path)
except HomeAssistantError as err:
LOGGER.error("Unable to load %s: %s", path, str(err))
return []
except FileNotFoundError:
return []
for dev_id, device in devices.items():
# Deprecated option. We just ignore it to avoid breaking change
device.pop('vendor', None)
try:
device = dev_schema(device)
device['dev_id'] = cv.slugify(dev_id)
except vol.Invalid as exp:
async_log_exception(exp, dev_id, devices, hass)
else:
result.append(Device(hass, **device))
return result
def update_config(path: str, dev_id: str, device: Device):
"""Add device to YAML configuration file."""
with open(path, 'a') as out:
device = {device.dev_id: {
ATTR_NAME: device.name,
ATTR_MAC: device.mac,
ATTR_ICON: device.icon,
'picture': device.config_picture,
'track': device.track,
CONF_AWAY_HIDE: device.away_hide,
}}
out.write('\n')
out.write(dump(device))
def get_gravatar_for_email(email: str):
"""Return an 80px Gravatar for the given email address.
Async friendly.
"""
import hashlib
url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar'
return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest())

View File

@@ -0,0 +1,184 @@
"""Device tracker helpers."""
import asyncio
from typing import Dict, Any, Callable, Optional
from types import ModuleType
import attr
from homeassistant.core import callback
from homeassistant.setup import async_prepare_setup_platform
from homeassistant.helpers import config_per_platform
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util import dt as dt_util
from homeassistant.const import (
ATTR_LATITUDE,
ATTR_LONGITUDE,
)
from .const import (
DOMAIN,
PLATFORM_TYPE_LEGACY,
CONF_SCAN_INTERVAL,
SCAN_INTERVAL,
SOURCE_TYPE_ROUTER,
LOGGER,
)
@attr.s
class DeviceTrackerPlatform:
"""Class to hold platform information."""
LEGACY_SETUP = (
'async_get_scanner',
'get_scanner',
'async_setup_scanner',
'setup_scanner',
)
name = attr.ib(type=str)
platform = attr.ib(type=ModuleType)
config = attr.ib(type=Dict)
@property
def type(self):
"""Return platform type."""
for methods, platform_type in (
(self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY),
):
for meth in methods:
if hasattr(self.platform, meth):
return platform_type
return None
async def async_setup_legacy(self, hass, tracker, discovery_info=None):
"""Set up a legacy platform."""
LOGGER.info("Setting up %s.%s", DOMAIN, self.type)
try:
scanner = None
setup = None
if hasattr(self.platform, 'async_get_scanner'):
scanner = await self.platform.async_get_scanner(
hass, {DOMAIN: self.config})
elif hasattr(self.platform, 'get_scanner'):
scanner = await hass.async_add_job(
self.platform.get_scanner, hass, {DOMAIN: self.config})
elif hasattr(self.platform, 'async_setup_scanner'):
setup = await self.platform.async_setup_scanner(
hass, self.config, tracker.async_see, discovery_info)
elif hasattr(self.platform, 'setup_scanner'):
setup = await hass.async_add_job(
self.platform.setup_scanner, hass, self.config,
tracker.see, discovery_info)
else:
raise HomeAssistantError(
"Invalid legacy device_tracker platform.")
if scanner:
async_setup_scanner_platform(
hass, self.config, scanner, tracker.async_see, self.type)
return
if not setup:
LOGGER.error("Error setting up platform %s", self.type)
return
except Exception: # pylint: disable=broad-except
LOGGER.exception("Error setting up platform %s", self.type)
async def async_extract_config(hass, config):
"""Extract device tracker config and split between legacy and modern."""
legacy = []
for platform in await asyncio.gather(*[
async_create_platform_type(hass, config, p_type, p_config)
for p_type, p_config in config_per_platform(config, DOMAIN)
]):
if platform is None:
continue
if platform.type == PLATFORM_TYPE_LEGACY:
legacy.append(platform)
else:
raise ValueError("Unable to determine type for {}: {}".format(
platform.name, platform.type))
return legacy
async def async_create_platform_type(hass, config, p_type, p_config) \
-> Optional[DeviceTrackerPlatform]:
"""Determine type of platform."""
platform = await async_prepare_setup_platform(
hass, config, DOMAIN, p_type)
if platform is None:
return None
return DeviceTrackerPlatform(p_type, platform, p_config)
@callback
def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
scanner: Any, async_see_device: Callable,
platform: str):
"""Set up the connect scanner-based platform to device tracker.
This method must be run in the event loop.
"""
interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
update_lock = asyncio.Lock()
scanner.hass = hass
# Initial scan of each mac we also tell about host name for config
seen = set() # type: Any
async def async_device_tracker_scan(now: dt_util.dt.datetime):
"""Handle interval matches."""
if update_lock.locked():
LOGGER.warning(
"Updating device list from %s took longer than the scheduled "
"scan interval %s", platform, interval)
return
async with update_lock:
found_devices = await scanner.async_scan_devices()
for mac in found_devices:
if mac in seen:
host_name = None
else:
host_name = await scanner.async_get_device_name(mac)
seen.add(mac)
try:
extra_attributes = \
await scanner.async_get_extra_attributes(mac)
except NotImplementedError:
extra_attributes = dict()
kwargs = {
'mac': mac,
'host_name': host_name,
'source_type': SOURCE_TYPE_ROUTER,
'attributes': {
'scanner': scanner.__class__.__name__,
**extra_attributes
}
}
zone_home = hass.states.get(hass.components.zone.ENTITY_ID_HOME)
if zone_home:
kwargs['gps'] = [zone_home.attributes[ATTR_LATITUDE],
zone_home.attributes[ATTR_LONGITUDE]]
kwargs['gps_accuracy'] = 0
hass.async_create_task(async_see_device(**kwargs))
async_track_time_interval(hass, async_device_tracker_scan, interval)
hass.async_create_task(async_device_tracker_scan(None))

View File

@@ -8,9 +8,10 @@ from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import intent, template, config_entry_flow
_LOGGER = logging.getLogger(__name__)
from .const import DOMAIN
DOMAIN = 'dialogflow'
_LOGGER = logging.getLogger(__name__)
SOURCE = "Home Assistant Dialogflow"
@@ -83,16 +84,6 @@ async def async_unload_entry(hass, entry):
async_remove_entry = config_entry_flow.webhook_async_remove_entry
config_entry_flow.register_webhook_flow(
DOMAIN,
'Dialogflow Webhook',
{
'dialogflow_url': 'https://dialogflow.com/docs/fulfillment#webhook',
'docs_url': 'https://www.home-assistant.io/components/dialogflow/'
}
)
def dialogflow_error_response(message, error):
"""Return a response saying the error message."""
dialogflow_response = DialogflowResponse(message['result']['parameters'])

View File

@@ -0,0 +1,13 @@
"""Config flow for DialogFlow."""
from homeassistant.helpers import config_entry_flow
from .const import DOMAIN
config_entry_flow.register_webhook_flow(
DOMAIN,
'Dialogflow Webhook',
{
'dialogflow_url': 'https://dialogflow.com/docs/fulfillment#webhook',
'docs_url': 'https://www.home-assistant.io/components/dialogflow/'
}
)

View File

@@ -0,0 +1,3 @@
"""Const for DialogFlow."""
DOMAIN = "dialogflow"

View File

@@ -1,6 +1,7 @@
{
"domain": "dialogflow",
"name": "Dialogflow",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/dialogflow",
"requirements": [],
"dependencies": [

View File

@@ -46,28 +46,27 @@ class DiscordNotificationService(BaseNotificationService):
import discord
discord.VoiceClient.warn_nacl = False
discord_bot = discord.Client(loop=self.hass.loop)
discord_bot = discord.Client()
images = None
if ATTR_TARGET not in kwargs:
_LOGGER.error("No target specified")
return None
if ATTR_DATA in kwargs:
data = kwargs.get(ATTR_DATA)
data = kwargs.get(ATTR_DATA) or {}
if ATTR_IMAGES in data:
images = list()
if ATTR_IMAGES in data:
images = list()
for image in data.get(ATTR_IMAGES):
image_exists = await self.hass.async_add_executor_job(
self.file_exists,
image)
for image in data.get(ATTR_IMAGES):
image_exists = await self.hass.async_add_executor_job(
self.file_exists,
image)
if image_exists:
images.append(image)
else:
_LOGGER.warning("Image not found: %s", image)
if image_exists:
images.append(image)
else:
_LOGGER.warning("Image not found: %s", image)
# pylint: disable=unused-variable
@discord_bot.event

View File

@@ -24,19 +24,14 @@ DOMAIN = 'discovery'
SCAN_INTERVAL = timedelta(seconds=300)
SERVICE_APPLE_TV = 'apple_tv'
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'
SERVICE_HOMEKIT = 'homekit'
SERVICE_HEOS = 'heos'
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'
@@ -51,15 +46,10 @@ SERVICE_WINK = 'wink'
SERVICE_XIAOMI_GW = 'xiaomi_gw'
CONFIG_ENTRY_HANDLERS = {
SERVICE_AXIS: 'axis',
SERVICE_DAIKIN: 'daikin',
SERVICE_DECONZ: 'deconz',
'esphome': 'esphome',
'google_cast': 'cast',
SERVICE_HEOS: 'heos',
SERVICE_HUE: 'hue',
SERVICE_TELLDUSLIVE: 'tellduslive',
SERVICE_IKEA_TRADFRI: 'tradfri',
'sonos': 'sonos',
SERVICE_IGD: 'upnp',
}
@@ -101,12 +91,22 @@ SERVICE_HANDLERS = {
}
OPTIONAL_SERVICE_HANDLERS = {
SERVICE_HOMEKIT: ('homekit_controller', None),
SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'),
}
DEFAULT_ENABLED = list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS)
DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS)
MIGRATED_SERVICE_HANDLERS = {
'axis': None,
'deconz': None,
'esphome': None,
'ikea_tradfri': None,
'homekit': None,
'philips_hue': None
}
DEFAULT_ENABLED = list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS) + \
list(MIGRATED_SERVICE_HANDLERS)
DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS) + \
list(MIGRATED_SERVICE_HANDLERS)
CONF_IGNORE = 'ignore'
CONF_ENABLE = 'enable'
@@ -153,6 +153,9 @@ async def async_setup(hass, config):
async def new_service_found(service, info):
"""Handle a new service if one is found."""
if service in MIGRATED_SERVICE_HANDLERS:
return
if service in ignored_platforms:
logger.info("Ignoring service: %s %s", service, info)
return

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