Compare commits

..

145 Commits

Author SHA1 Message Date
Paulus Schoutsen d9a6d9ee73 Merge pull request #10630 from home-assistant/release-0-58
0.58
2017-11-17 22:27:35 -08:00
Robbie Trencheny 425c027085 Implement entity and domain exclude/include for Alexa (#10647)
* Implement entity and domain exclude/include for Alexa

* Switch to using generate_filter

* Use proper domain for turn on/off calls except for groups where we must use the generic homeassistant.turn_on/off

* travis fixes

* Untangle

* Lint
2017-11-17 22:00:11 -08:00
Pierre Ståhl 35699273da Bump pyatv to 0.3.8 (#10643)
Fixes AirPlay issues on newer versions of tvOS.
2017-11-17 22:00:11 -08:00
Andrey b86110a15d Print entity type in "too slow" warnings (#10641)
* Update entity.py

* Update entity.py
2017-11-17 22:00:10 -08:00
Robbie Trencheny e449ceeeff Alexa improvements (#10632)
* Initial scene support

* Initial fan support

* ordering

* Initial lock support

* Scenes cant be deactivated; Correct the scene display category

* Initial input_boolean support

* Support customization of Alexa discovered entities

* Initial media player support

* Add input_boolean to tests

* Add play/pause/stop/next/previous to media player

* Add missing functions and pylint

* Set manufacturerName to Home Assistant since the value is displayed in app

* Add scene test

* Add fan tests

* Add lock test

* Fix volume logic

* Add volume tests

* settup -> setup

* Remove unused variable

* Set required scene description as per docs

* Allow setting scene category (ACTIVITY_TRIGGER/SCENE_TRIGGER)

* Add alert, automation and group support/tests

* Change display categories to match docs

* simplify down the display category props into a single prop which can be used on any entity

* Fix tests to expect proper display categories

* Add cover support

* sort things

* Use generic homeassistant domain for turn on/off
2017-11-17 22:00:10 -08:00
Cezar Sá Espinola bf8e2bd77e Make MQTT reconnection logic more resilient and fix race condition (#10133) 2017-11-17 22:00:10 -08:00
Aaron Bach 0202e966ea Fixes AirVisual bug regarding incorrect location data (#10054)
* Fixes AirVisual bug regarding incorrect location data

* Owner-requested changes
2017-11-17 22:00:09 -08:00
Paulus Schoutsen b3d66e5881 Update frontend to 20171118.0 2017-11-17 19:09:47 -08:00
Paulus Schoutsen eb8a8f6d0b Version bump to 0.58.0 2017-11-16 22:10:40 -08:00
Paulus Schoutsen 62c8843956 Update frontend to 20171117.1 2017-11-16 22:08:19 -08:00
John Arild Berentsen 1bb37aff0c Add loglinefetch for frontend API call (#10579)
* Add loglinefetch for frontend API call

* Too many blank lines

* Review changes

* review changes

* Only return a text

* Use aiohttp

* Don't do I/O in event loop

* Move lines to query and default to 0

* Small fixes
2017-11-16 22:07:08 -08:00
Egor Tsinko f052a0926b Added sorted() to python_script (#10621)
* added sorted() to python_script

* fixed lint errors
2017-11-16 22:06:02 -08:00
Anders Melchiorsen 24aeea5ca3 Adjust logging in downloader component (#10622) 2017-11-16 22:05:08 -08:00
Paulus Schoutsen 5c20cc32b5 Update frontend to 20171117.0 2017-11-16 22:03:31 -08:00
Corey Pauley 6cf2e758a8 Alexa slot synonym fix (#10614)
* Added logic to the alexa component for handling slot synonyms

* Moved note with long url to the top of the file

* Just made a tiny url instead of messing with Flake8

* Refactored to be more Pythonic

* Put trailing comma back
2017-11-16 21:09:00 -08:00
Adam Mills aa6b37912a Fix async missing decorators (#10628) 2017-11-16 21:03:05 -08:00
Jan Losinski 693d32fa68 Snapcast: bump version and enable reconnect. (#10626)
This bumps the used snapcast version to 2.0.8 and enables the new
reconnect feature that causes the component to reconnect to a server if
the connection was lost.

This fixes the ned to restart Home Assstant after a snapcast reboot, as
described in issue #10264.

Signed-off-by: Jan Losinski <losinski@wh2.tu-dresden.de>
2017-11-16 20:32:26 -05:00
Andrey 072ed7ea13 Allow to pass YandexTTS options via sevice call (#10578) 2017-11-16 09:10:25 -08:00
Mitko Masarliev bd5a16d70b update hbmqtt to 0.9.1 (#10611) 2017-11-16 07:47:37 -08:00
Milan V eb7643e163 Improve WUnderground config validation (#10573)
* Fix WUnderground config validation

* Fix indentation
2017-11-16 10:26:23 -05:00
Milan V 79ca93f892 Change generic thermostat to control heating on mode change Off -> Auto (#10601)
* Change generic thermostat to control heating on mode change Off -> Auto

* Fix typo
2017-11-16 13:11:46 +01:00
Colin Dunn 3dbae5ca5b Correct input_datetime initial value parsing (#10417)
* Correct input_datetime initial value parsing

* Correct input_datetime initial value parsing
2017-11-15 23:16:22 -08:00
Pascal Vizeli 1719fa7008 Cleanup old stale restore feature (#10593)
* Cleanup old stale restore feature

* cleanup

* Update __init__.py

* Update test_demo.py

* Lint
2017-11-15 23:03:41 -08:00
Per Osbäck d4bd4c114b add support for color temperature and color to Google Assistant (#10039)
* add support for color temperature and color; also add some extra deviceInfo attributes

* change so that default behaviour doesn't turn off device if the action isn't handled

* add tests

* fix lint

* more lint

* use attributes were applicable

* removed debug logging

* fix unassigned if only None returned

* report more data in QUERY

* better tests for color and temperature

* fixes after dev merge

* remove deviceInfo as not part of a device state (PR #10399)

* fix after merge
2017-11-15 23:00:43 -08:00
boltgolt f494c32866 Small fix to be able to use mac and vendor in "device_tracker_new_device" event. (#10537)
* Small fix to be able to use mac and vendor in EVENT_NEW_DEVICE event

* Missed device_tracker test
2017-11-15 22:41:39 -08:00
Fabian Affolter e20fd3b973 Upgrade mypy to 0.550 (#10591) 2017-11-15 22:35:18 -08:00
ziotibia81 270846c2f5 Modbus switch register support (#10563)
* Update modbus.py

* Fix blank linea and whitespaces

* Fix visual indent

* Fix visual indent

* fix multiple statements on one line

* Typo

* Disable pylint check

# pylint: disable=super-init-not-called

* Fix code style
2017-11-15 22:17:10 -08:00
Fabrizio Furnari b2ab4443a7 New sensor viaggiatreno. (#10522)
* New sensor viaggiatreno.

I've messed up the previous PR so here it is in a new one.
Should include also all corrections from @pvizeli

* fixes from PR 10522

* fixed import order

* requested changes from MartinHjelmare
2017-11-15 22:07:16 -08:00
Craig J. Ward 17cd64966d bump client version (#10610) 2017-11-15 22:04:26 -08:00
Michael Chang 48181a9388 Support script execution for Alexa (#10517)
* Support script execution for Alexa

* Use PowerController for the script component
2017-11-15 21:44:27 -08:00
Andrey d5cba0b716 Allow unicode when dumping yaml (#10607) 2017-11-15 18:24:08 -08:00
Alok Saboo 3a0c749a12 Fix Hikvision (motion) switch bug (#10608)
* Fix Hikvision switch bug

* Added comment about last working version
2017-11-16 01:15:45 +01:00
ziotibia81 d652d793f3 Fix ValueError exception (#10596)
* Fix ValueError exception

structure = '>{:c}'.format(data_types[register.get(CONF_DATA_TYPE)][register.get(CONF_COUNT)])
give:
ValueError: Unknown format code 'c' for object of type 'str'

* Minor typo
2017-11-15 18:17:17 -05:00
Pierre Ståhl 87995ad62c Do not add panel from system_log (#10600)
The frontend will not have this panel.
2017-11-15 23:45:08 +01:00
Jeremy Williams c2d0c8fba4 Arlo - Fixes for updated library (#9892)
* Reduce update calls to API. Add signal strength monitor.

* Fix lint errors

* Fix indent

* Update pyarlo version and review fixes

* Fix lint errors

* Remove staticmethod

* Clean up attributes

* Update arlo.py
2017-11-15 23:33:50 +01:00
On Freund c7b0f25eae Fix Yahoo Weather icons over SSL (#10602) 2017-11-15 22:27:26 +02:00
Fabian Affolter d5b170f761 Upgrade youtube_dl to 2017.11.15 (#10592) 2017-11-15 12:41:25 +01:00
Paulus Schoutsen ea7ffff0ca Cloud updates (#10567)
* Update cloud

* Fix tests

* Lint
2017-11-15 08:16:19 +01:00
Paulus Schoutsen 0cd3271dfa Update frontend to 20171115.0 2017-11-14 22:48:31 -08:00
Paulus Schoutsen 7920ddda9d Add panel build type (#10589) 2017-11-14 22:39:06 -08:00
NovapaX 1e493dcb8a Tradfri unique identities (#10414)
* Unique identity
Use unique ID for generating keys and store them in config. Fallback to
old id so existing installs will still work.

* Remove Timeouts
they don't really work. this should be fixed in pytradfri I think.

* import uuid only when necessary

* more selective import

* lint

* use load_json and save_json from util.json

* remove unnecessary imports

* use async configurator functions

* async configurator calls

* thou shalt not mixup the (a)syncs

* again: no asyncs in the syncs!
last warning...

* Update tradfri.py
2017-11-14 22:16:21 -08:00
Pierre Ståhl 8111e3944c Add basic backend support for a system log (#10492)
Everything logged with "warning" or "error" is stored and exposed via
the HTTP API, that can be used by the frontend.
2017-11-14 20:35:56 -08:00
NovapaX 8d91de877a turn service call handler into coroutine (#10576) 2017-11-14 20:32:48 -08:00
Eitan Mosenkis 0b4de54725 Google Assistant for climate entities: Support QUERY and respect system-wide unit_system setting. (#10346) 2017-11-14 20:19:42 -08:00
marthoc 309e493e76 Add code to enable discovery for mqtt cover (#10580)
* Add code to enable discovery for mqtt cover

* Fix pylint error
2017-11-14 20:19:15 -08:00
Marcelo Moreira de Mello 95c831d5bc Bump ring_doorbell to 0.1.7 (#10566) 2017-11-14 15:56:42 +01:00
Andreas Björshammar 061253fded Verisure: Added option to set installation giid (#10504)
* Added option to set installation giid

* Changed where giid config var is being checked

* Style fix

* Fix style
2017-11-14 15:53:26 +01:00
Eugenio Panadero e947e6a143 Use a template for the Universal media player state (#10395)
* Implementation of `state_template` for the Universal media_player

* add tracking to entities in state template

* use normal config_validation

* fix tests, use defaults in platform schema, remove extra keys

* and test the new option `state_template`

* lint fixes

* no need to check attributes against None

* use `async_added_to_hass` and call `async_track_state_change` from `hass.helpers`
2017-11-14 11:41:19 +01:00
Martin Hjelmare dc6e50c39d Fix lametric sound (#10562)
* Fix sound for lametric notify

* Remove not used method
2017-11-14 10:40:44 +01:00
Abílio Costa 637b058a7e webostv: Reduce default timeout to prevent log spamming (#10564)
With the default timeout of 10 seconds, the log gets filled up with "component is taking more than 10 seconds" errors.
This should probably be fixed in some other way, but for now this reduces the problem a bit.
2017-11-14 10:37:52 +01:00
Fabian Affolter d25f676711 Move temperature display helper from components to helpers (#10555) 2017-11-14 10:36:18 +01:00
Steve Edson b1afed9e52 pad packets to multiple of 4 characters (#10560)
* pad packets to multiple of 4 characters

This fixes sending commands, see #7669

* Update broadlink.py

* removed whitespace
2017-11-14 09:18:06 +01:00
Kenny Millington 7c24d77031 Don't use the 'id' field since it can be autogenerated (fixes #10551). (#10554) 2017-11-13 22:46:26 -08:00
ziotibia81 e33451e2b9 Better support for int types (#10409)
* Better int types support

* type

* Added optional register order

* Fix white spaces

* Fix line length

* Fix line too long

* Fix trailing whitespace

* Stylistc code fixes
2017-11-13 23:27:15 +01:00
Ari Lotter 2dcde12d38 Support presence detection using Hitron Coda router (#9682)
* Support presence detection using Hitron Coda router

* at least 2 spaces before inline comment

* Update hitron_coda.py

* rewrote authentication code, it actually works now

* make line slightly shorter to comply with hound

* Removed hardcoded IP address

* Fix string formatting, add timeout, and use generator

* Update hitron_coda.py

* Update hitron_coda.py

* Update hitron_coda.py

* typo

* update .coveragerc

* Update stale URL
2017-11-13 23:10:39 +01:00
Martin Hjelmare 3c135deec8 Fix and clean lametric (#10391)
* Fix and clean lametric

* Add missing DEPENDENCIES in notify platform.
* Remove not needed method in component manager class.
* Don't overwrite notify DOMAIN.
* Return consistently depending on found devices in setup of component.

* Get new token if token expired

* Add debug log for getting new token

* Clean up
2017-11-13 21:12:15 +01:00
Fabian Affolter 6974f2366d Upgrade pysnmp to 4.4.2 (#10539) 2017-11-13 09:24:07 -08:00
Ruslan Sayfutdinov a6d9c7a621 webostv: set current source correctly (#10548) 2017-11-13 09:23:42 -08:00
Anders Melchiorsen 46fe9ed200 Optimize concurrent access to media player image cache (#10345)
We now do locking to ensure that an image is only downloaded and added
once, even when requested by multiple media players at the same time.
2017-11-13 09:03:12 -08:00
r4nd0mbr1ck f6d511ac1a Google Assistant request sync service (#10165)
* Initial commit for request_sync functionality

* Fixes for Tox results

* Fixed all tox issues and tested locally with GA

* Review comments - api_key, conditional read descriptions

* Add test for service
2017-11-13 08:32:23 -08:00
Diogo Gomes bc23799c71 Change to device state attributes (#10536)
* Following the suggestion of @MartinHjelmare
2017-11-12 15:25:44 +01:00
Per Osbäck 59e943b3c1 notify.html5: use new json save and load functions (#10416)
* update to use new save_json and load_json

* it is no longer possible to determine if the json file contains valid or empty data.

* fix lint
2017-11-11 15:57:11 -08:00
Paulus Schoutsen c8648fbfb8 Pre-construct frontend index.html (#10520)
* Pre-construct frontend index.html

* Cache templates

* Update frontend to 20171111.0

* Fix iframe panel test
2017-11-11 15:22:05 -08:00
Vignesh Venkat 96e7944fa8 telegram_bot: Support for sending videos (#10470)
* telegram_bot: Support for sending videos

Telegram python library has a sendVideo function that can be used
similar to sending photos and documents.

* fix lint issue

* fix grammar
2017-11-12 00:13:35 +01:00
Aaron Bach 79001fc361 Adds support for Tile® Bluetooth trackers (#10478)
* Initial work in place

* Added new attributes + client UUID storage

* Wrapped up

* Collaborator-requested changes
2017-11-11 23:21:03 +01:00
Paulus Schoutsen 2310b791f9 Merge branch 'master' into dev 2017-11-11 13:04:55 -08:00
Paulus Schoutsen d814d40330 Merge pull request #10534 from home-assistant/release-0-57-3
0.57.3
2017-11-11 13:00:19 -08:00
William Scanlon b6e098d1c2 Fixed Wink Quirky Aros bugs. (#10533)
* Fixed Wink Quirky Aros bugs.
2017-11-11 15:49:20 -05:00
Martin Berg db56748d88 Add attribute to show who last un/set alarm (SPC) (#9906)
* Add attribute to show who last un/set alarm.

This allows showing the name of the SPC user who last
issued an arm/disarm command and also allows for
automations to depend on this value.

* Optimize

* Update spc.py

* Update spc.py

* fix

* Fix test.

* Fix for removed is_state_attr.
2017-11-11 12:36:03 -08:00
Kane610 68fb995c63 Update axis.py (#10412) 2017-11-11 12:30:18 -08:00
Andrey 4420f11d9d Fix import in tests (#10525) 2017-11-11 22:24:43 +02:00
Erik Eriksson 75836affbe Support configuration of region (no service url neccessary (#10513) 2017-11-11 12:21:25 -08:00
Lukas Barth b284cc54df Pin yarl (#10528)
* Pin yarl

* Update requirements
2017-11-11 12:15:13 -08:00
Paulus Schoutsen 547e089185 Version bump to 0.57.3 2017-11-11 12:14:28 -08:00
Marcelo Moreira de Mello fe2e0c44c8 Fixed update() method and removed ding feature from stickupcams/floodlight (#10428)
* Simplified URL expiration calculation and fixed refresh method

* Remove support from Ring from StickupCams or floodlight cameras

* Makes lint happy

* Removed unecessary attributes
2017-11-11 12:12:58 -08:00
Stefan Jonasson 30bd92c851 Tellstick Duo acync callback fix (#10384)
* Reverted commit 1c8f179690. This fixes issue: #10329

* convert callback to async

* fix lint

* cleanup

* cleanup

* cleanups

* optimize initial handling

* Update tellstick.py

* Update tellstick.py

* fix lint

* fix lint

* Update tellstick.py

* Fixed code errors and lint problems.

* fix bug

* Reduce logic, migrate to dispatcher

* Update tellstick.py

* Update tellstick.py

* fix lint

* fix lint
2017-11-11 12:12:57 -08:00
Paulus Schoutsen 78afbd4292 Pin YARL to 0.13 2017-11-11 12:12:31 -08:00
Hmmbob f3a90d6994 Update nederlandse_spoorwegen.py to include platform information (#10494)
* Update nederlandse_spoorwegen.py

Make departure and arrival platforms available as state attributes

* Update nederlandse_spoorwegen.py

* Update nederlandse_spoorwegen.py
2017-11-11 11:51:26 -08:00
Lukas Barth 44506ce15f Adapt to new yarl API (#10527) 2017-11-11 08:36:37 -08:00
Andrey 5e92fa3404 Add an option to serve ES6 JS to clients (#10474)
* Add an option to serve ES6 JS to clients

* Rename es6 to latest

* Fixes

* Serve JS vrsions from separate dirs

* Revert websocket API change

* Update frontend to 20171110.0

* websocket: move request to constructor
2017-11-10 23:02:06 -08:00
Jan Almeroth 1c36e2f586 Introduce media progress for Yamaha Musiccast devices (#10256)
* Introduce update_hass()

* Introduce media_positions

* Version bump pymusiccast

* Fix: Unnecessary "else" after "return"

* FIX D400: First line should end with a period

* Version bump

Fixes https://github.com/home-assistant/home-assistant/issues/10411
2017-11-10 23:41:02 +01:00
Kenny Millington 16dd90ac78 Add support for Alexa intent slot synonyms. (#10469) 2017-11-10 09:35:57 -08:00
Eric Hagan 7d9d299d5a OwnTracks Message Handling (#10489)
* Improve handling and logging of unsupported owntracks message types

Added generic handlers for message types that are valid but not
supported by the HA component (lwt, beacon, etc.) and for
message types which are invalid. Valid but not supported
messages will now be logged as DEBUG. Invalid messages will
be logged as WARNING.

Supporting single "waypoint" messages in addition to the
roll-up "waypoints" messages.

Added tests around these features.

* Style fixes
2017-11-10 09:29:21 -08:00
Fabian Affolter 0490ca67d1 Bump dev to 0.58.0.dev0 (#10510) 2017-11-10 09:25:31 -08:00
Matthew Donoughe e7dc96397c upgrade to new pylutron_caseta with TLS (#10286)
* upgrade to new pylutron with TLS

* rename configuration options

* change more methods to coroutines

* use async_add_devices
2017-11-10 12:17:25 +01:00
Diogo Gomes 9bfdff0be1 add JSON processing capabilities to sensor_serial (#10476)
* add JSON processing capabilities

* format fixes

* format fixes

* Fix according to @fabaff comment

* reverting last commit to a more sane approach

* docstring...

* still docstring...

* passed script/lint

* downgrade exception

JSONDecodeError was only introduced in Python3.5

Since we are still supporting 3.4 ValueError is the parent class of
JSONDecodeError
2017-11-10 10:49:30 +01:00
sander76 143d9492b2 Fix for telegram polling. (added pausing when error occurs) (#10214)
* Fix for telegram polling. (added pausing when error occurs)

* fix pylint error.
invalid variable name ( Exception as _e)). Don't understand why as
removing the underscore fails with my local pylint..

* fixing too short variable name.

* moved logic to `check_incoming`

* fix line too long error.

* Simplify
2017-11-09 21:17:23 +01:00
Fabian Affolter 8e1a73dd0f Upgrade youtube_dl to 2017.11.06 (#10491) 2017-11-09 20:18:29 +01:00
Fabian Affolter 8878eccb7b Upgrade psutil to 5.4.1 (#10490) 2017-11-09 20:17:31 +01:00
cgtobi 37eae7fb8a Improve error handling. (#10482)
* Improve error handling.

* Fix import of core requirements.

* cleanup
2017-11-09 20:17:01 +01:00
Anders Melchiorsen dd16b7cac3 Remove lag from Harmony remote platform (#10218)
* Simplify kwargs handling

* Move Harmony remote to a persistent connection with push feedback

* Make default delay_secs configurable on the harmony platform

* Remove lint

* Fix delay_secs with discovery

* Temporary location for updated pyharmony

* Remove lint

* Update pyharmony to 1.0.17

* Remove lint

* Return an Optional marker

* Update pyharmony to 1.0.18
2017-11-09 17:57:41 +01:00
David Grant 68986e9143 Updated gc100 package requirement to 1.0.3a (#10484)
* Updated gc100 package requirement to 1.0.3a

* Update requirements_all.txt
2017-11-09 17:54:45 +01:00
Stefan Jonasson 62c1b542ed Tellstick Duo acync callback fix (#10384)
* Reverted commit 1c8f179690. This fixes issue: #10329

* convert callback to async

* fix lint

* cleanup

* cleanup

* cleanups

* optimize initial handling

* Update tellstick.py

* Update tellstick.py

* fix lint

* fix lint

* Update tellstick.py

* Fixed code errors and lint problems.

* fix bug

* Reduce logic, migrate to dispatcher

* Update tellstick.py

* Update tellstick.py

* fix lint

* fix lint
2017-11-09 15:03:35 +01:00
Julius Mittenzwei ee265394a6 Improvement of KNX climate component (#10388)
* Added myself to codeowners

* Improved climate support with setpoint shift for KNX. (https://github.com/XKNX/xknx/issues/48)

* requirements_all.txt

* typo

* flake

* Changes requested by @pvizeli
2017-11-09 11:49:19 +01:00
TopdRob 9297a9cbb4 Upgrade apns2 to 0.3.0 (#10347) 2017-11-08 21:09:19 -08:00
Marcelo Moreira de Mello 2118ab2503 Fixed update() method and removed ding feature from stickupcams/floodlight (#10428)
* Simplified URL expiration calculation and fixed refresh method

* Remove support from Ring from StickupCams or floodlight cameras

* Makes lint happy

* Removed unecessary attributes
2017-11-09 01:01:20 +01:00
Pascal Vizeli 2fff065b2c Remove useless temp converting (#10465) 2017-11-09 00:46:33 +01:00
TopdRob ed9abe3fa2 Upgrade pyatv to 0.3.6 (#10349)
Fix string conversion for idle state
2017-11-08 16:13:05 +01:00
TopdRob f5ea7d3c9c Upgrade to 0.1.2 (#10348)
Fix an insecure request warning when not using verify=True. Contributed by @nalepae
2017-11-08 16:11:12 +01:00
Matt White 148a7ddda9 Add include/exclude filter to mqtt_statestream (#10354)
* Add publish filter to mqtt_statestream

* Add tests for include/excludes in mqtt_statestream
2017-11-08 15:54:12 +01:00
Milan V 2f0920e4fb Fix recorder stop on SQLite vacuuming error (#10405)
* Fix recorder stop on SQLite vacuuming error

* Move import to function
2017-11-08 14:43:15 +01:00
Paulus Schoutsen 2e5b1e76ef Fix slow WOL switch test (#10455) 2017-11-08 12:38:17 +01:00
Per Osbäck db8510f110 update pywebpush==1.3.0 (#10374) 2017-11-08 12:02:28 +01:00
Daniel Høyer Iversen e49278cc7d update tibber library (#10460) 2017-11-08 11:18:35 +01:00
Paulus Schoutsen 50f6790a27 Remove model info from state (#10399) 2017-11-07 21:28:11 -08:00
Diogo Gomes a5aa111893 Add baudrate option to Serial sensor (#10439)
* Add baudrate option

Baudrate is essential!

* line too long

line too long (82 > 79 characters)

* trailing whitespace

* Rename const

* Fix the missing one
2017-11-07 22:06:19 +01:00
Robin 119fb08198 Fixes issue #10425 (#10426)
Fixes an error reported resulting from Hammersmith no longer supplying
data.
2017-11-07 18:19:54 +01:00
Adam Mills 11ecc2c171 Remove extra info from zwave entity states (#10413)
* Remove extra info from zwave entity states

* Show initializing for nodes that haven't completed queries
2017-11-07 10:13:39 -05:00
Mister Wil 07f073361f Bump to 0.12.2 to fix urllib3 dependency (#10420) 2017-11-07 01:39:13 +01:00
John Arild Berentsen 5410700708 Zwave save cache to file now. (#10381)
* Add save config

* Add API to save Z-Wave cache to file immediatley.

* lint

* remove none assignment

* docstring
2017-11-06 15:15:52 +01:00
Sebastian Muszynski 131af1fece Device model identification of the Xiaomi Philips Ceiling Lamp fixed. (#10401) 2017-11-06 09:20:31 +01:00
Pascal Vizeli a9a3e24bde Update aiohttp to 2.3.1 (#10139)
* Update aiohttp to 2.3.1

* set timeout 10sec

* fix freeze with new middleware handling

* Convert middleware auth

* Convert mittleware ipban

* convert middleware static

* fix lint

* Update ban.py

* Update auth.py

* fix lint

* Fix tests
2017-11-05 18:42:31 -08:00
Paulus Schoutsen 39de557c4c Update frontend 2017-11-05 18:26:16 -08:00
Paulus Schoutsen 4742899369 Merge remote-tracking branch 'origin/master' into dev 2017-11-05 18:23:08 -08:00
Fabian Affolter f3511d615e Upgrae simplepush to 1.1.4 (#10365) 2017-11-05 22:52:58 +01:00
Simon 5d4514652d Addition of new binary sensor class 'plug' (#10336)
* Addition of new binary sensor class 'plug'

* use term "unplugged"

* add the entry to the right place
2017-11-05 10:25:44 -08:00
John Arild Berentsen c07e651013 Add heal_node and test_node services. (#10369)
* Add heal_node and test_node services.

* lint
2017-11-05 09:19:19 -08:00
Patrik bc51bd93f4 Fix tradfri problem with brightness (#10359)
* Fix problem with brightness

* Fix typo

* Typo
2017-11-05 17:43:45 +01:00
Adam Cooper 72ce9ec321 Add platform and sensors for Vultr VPS (#9928)
* Initial commit of Vultr components

Have a working Vultr hub and binary sensor which pulls down the
following attributes of your VPS:
 - Date created
 - Subscription id (server id)
 - Cost per month (in US$)
 - Operating System installed
 - IPv4 address
 - label (human readable name)
 - region
 - number of vcpus
 - which storage package chosen
 - IPV6 address (if applicable)
 - RAM amount

Working next on sensor and then testing / coverage.

* Added Vultr sensor for pending charges and current bandwidth. Refactored binary_sensor and hub too

* Corrected is_on bases

* Added basic tests for Vultr binary & platform

* Updated require files

* Changing test fixture to highlight different cases

* Written basic test for sensor.vultr

* Resolved linting errors and broken test

* Increase test coverage and corrected docs

* Resolved hound issues

* Revert back negative binary test

* Another hound resolve

* Refactoring and adding is switch, moving over to vultr branch

* Made Vultr components more resiliant to invalid configs

* Added negetive test for vultr binary sensor

* Added better testing of vultr sensor

* Resolved vultr platform test affecting subsequent vultr tests

* Moving VULTR components to single use design

* Added in sensor name config

* Added missing sensors var

* Resolved init data setting of sensors, added in name conf to switch

* Made the Vultr component more resiliant to startup failure with better alerting

* Various Vultr component changes

- Refactored sensor, binary_sensor, and switch to reference one subscription
- Renamed CURRENT_BANDWIDTH_GB monitored condition to CURRENT_BANDWIDTH_USED
- Improved test coverage

* Resolved local tox linting issue

* Added more testing for Vultr switch

* Improved test coverage for Vultr components

* Made PR comment changes to vultr binary sensor

* Made PR comment changes to Vultr sensor

* resolved PR comments for Vultr Switch

* Resolved vultr sensor name and improved tests

* Improved Vultr switch testing (default name formatting)

* Removed vultr hub failure checking
2017-11-05 14:10:14 +01:00
Fabian Affolter a5d5f3f727 Move counter component (#10332)
* Fix docstring

* Add comment

* Move counter to folder

* Fix missing parts

* Commit it when file is saved
2017-11-05 13:51:52 +01:00
Fabian Affolter 5be6f8ff36 Upgrade sqlalchemy to 1.1.15 (#10330) 2017-11-05 13:51:03 +01:00
Per Osbäck 28ef564974 fix a import in test causing vs code to fail to discover (#10358)
* fix a import in test causing vs code to fail to discover

* Change style
2017-11-05 13:50:46 +01:00
Fabian Affolter de9d19d6f4 Use constants for HTTP headers (#10313)
* Use constants for HTTP headers

* Fix ordering

* Move 'no-cache' to platform
2017-11-04 12:04:05 -07:00
marconfus e64803e701 Fix for API change of new enocean package (#10328)
* Fix API change of new enocean package

* Fix lint issue
2017-11-04 12:58:02 +01:00
Pascal Vizeli 0f7a4b1d6f Move timer into correct folder (#10324)
* Move timer into correct folder

* Rename tests/components/test_timer.py to tests/components/timer/test_timer.py

* create init for test component

* Fix services.yaml loading
2017-11-03 21:10:08 -07:00
Craig J. Ward acfee385fb Tc update (#10322)
* use updated client

* update requirements
2017-11-03 20:46:40 -07:00
Alok Saboo 96657841c8 Add option to overwrite file to the downloader component (#10298)
* Add option to overwrite file to the downloader component

* Cleanup

* Address Paulus's comments
2017-11-03 13:02:38 -07:00
Pascal Vizeli a4dec0b6d2 Fix recorder purge (#10318)
* Fix recorder purge

* Fix lint

* fix utc convert
2017-11-03 12:55:00 -07:00
Pascal Vizeli 06d3d8b827 TellStick / Remove async flavor / add hassio (#10315)
* Remove unused async flavor

* Add tellcore-net support

* Update tellstick.py

* Update requirements_all.txt

* fix lint
2017-11-03 20:31:48 +01:00
Paulus Schoutsen 0877ea07b3 Fix formatting invalid config text (#10319) 2017-11-03 18:12:45 +01:00
William Scanlon 31b89f602a Strip white space from configurator input (#10317) 2017-11-03 08:58:03 -07:00
PeteBa 1ffccfc91c Maintain recorder purge schedule (#10279)
* Maintain automated purge schedule

* Updates from review feedback
2017-11-03 08:28:16 -07:00
Fabian Affolter 81324806d5 Move constants to setup.py (#10312)
* Remove unused import

* Move setup relevant consts to 'setup.py'

* remove blank line

* Set source
2017-11-03 07:43:30 -07:00
Sebastian Muszynski a43f99a71c Allow an empty MAC address at the Xiaomi Aqara Gateway configuration. (#10307) 2017-11-03 07:38:15 -07:00
Hugo Dupras 1347c3191f Refactor Neato botvac components as a vacuum (#9946)
* Refactor Neato botvac components as a vacuum

A switch is still use to enable/disable the schedule

Signed-off-by: Hugo D. (jabesq) <jabesq@gmail.com>

* CI Hound fixes

* Fix lint errors

Signed-off-by: Hugo D. (jabesq) <jabesq@gmail.com>

* [Neato vacumm] Add sensor attributes to vacuum

Signed-off-by: Hugo D. (jabesq) <jabesq@gmail.com>

* Remove line breaks and fix docstring

* PR fixes
2017-11-03 14:25:26 +01:00
Paulus Schoutsen 4e8e04fe66 Clean up core (#10305)
* Clean up core

* Lint

* Fix tests

* Address comment

* Update entity.py

* romve test for forward update to async_update

* fix lint
2017-11-03 14:19:36 +01:00
Fabian Affolter 9b8c64c8b6 Upgrade credstash to 1.14.0 (#10310) 2017-11-03 13:51:17 +01:00
Paulus Schoutsen a943b207ba Fix panel_custom (#10303)
* Fix panel_custom

* lint
2017-11-03 10:28:31 +01:00
Heiko Thiery 23809bff64 Add LaCrosse sensor platform (#10195)
* Initial commit of LaCrosse sensor component

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>

* fix review comments from houndci-bot

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>

* fix review comments from houndci-bot

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>

* add pylacrosse version to REQUIREMENTS

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>

* add lacrosse to .coveragerc

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>

* import 3rd party libraries inside methods

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>

* add pylacrosse to requirements_all.txt

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>

* add missing docstring

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>

* fix pylint warning

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>

* fix pylint warning

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>

* fix pylint warnings

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>

* remove too many blank lines

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>

* some minor cleanup

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>

* change to single quote

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>

* incorporate review comments

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>

* remove type check as validation only allows TYPES

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>

* Adjust log level and update ordering
2017-11-03 08:59:11 +01:00
Paulus Schoutsen a4f7828363 Cloud: Authenticate with id token (#10304) 2017-11-03 07:30:05 +01:00
Paulus Schoutsen 2598770b49 Update frontend 2017-11-02 22:52:19 -07:00
Paulus Schoutsen 8f774e9c53 Cleanup Xiaomi Aqara (#10302) 2017-11-02 22:18:10 -07:00
NovapaX 47d9403e3a update mask-icon to a working mask-icon.svg (#10290)
* update mask-icon to favicon.svg

* change name of icon to mask-icon.svg
2017-11-02 20:55:09 -07:00
Markus 4d19092722 pyLoad download sensor (#10089)
* Create pyload.py

* tabs and whitespaces removed

* code style fix

* code style fixes

* code style fix

* fixed standard import order

* classname fixed

* Added homeassistant/components/sensor/pyload.py

* code formatting

* implemented @fabaff recommendations

* Update pyload.py

* Use string formatting

* Make host optional
2017-11-02 22:17:44 +01:00
Sebastian Muszynski f2a38677fc Bump python-miio for improved device support (#10294)
* Bump python-miio for improved device support.

* Requirements defines updated.
2017-11-02 21:38:18 +01:00
235 changed files with 7351 additions and 2610 deletions
+5
View File
@@ -309,6 +309,7 @@ omit =
homeassistant/components/device_tracker/cisco_ios.py
homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/gpslogger.py
homeassistant/components/device_tracker/hitron_coda.py
homeassistant/components/device_tracker/huawei_router.py
homeassistant/components/device_tracker/icloud.py
homeassistant/components/device_tracker/keenetic_ndms2.py
@@ -325,6 +326,7 @@ omit =
homeassistant/components/device_tracker/thomson.py
homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/tado.py
homeassistant/components/device_tracker/tile.py
homeassistant/components/device_tracker/tplink.py
homeassistant/components/device_tracker/trackr.py
homeassistant/components/device_tracker/ubus.py
@@ -517,6 +519,7 @@ omit =
homeassistant/components/sensor/influxdb.py
homeassistant/components/sensor/irish_rail_transport.py
homeassistant/components/sensor/kwb.py
homeassistant/components/sensor/lacrosse.py
homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/linux_battery.py
homeassistant/components/sensor/loopenergy.py
@@ -545,6 +548,7 @@ omit =
homeassistant/components/sensor/pocketcasts.py
homeassistant/components/sensor/pushbullet.py
homeassistant/components/sensor/pvoutput.py
homeassistant/components/sensor/pyload.py
homeassistant/components/sensor/qnap.py
homeassistant/components/sensor/radarr.py
homeassistant/components/sensor/ripple.py
@@ -579,6 +583,7 @@ omit =
homeassistant/components/sensor/upnp.py
homeassistant/components/sensor/ups.py
homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/viaggiatreno.py
homeassistant/components/sensor/waqi.py
homeassistant/components/sensor/whois.py
homeassistant/components/sensor/worldtidesinfo.py
+4
View File
@@ -64,6 +64,10 @@ homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/*/rfxtrx.py @danielhiversen
homeassistant/components/velux.py @Julius2342
homeassistant/components/*/velux.py @Julius2342
homeassistant/components/knx.py @Julius2342
homeassistant/components/*/knx.py @Julius2342
homeassistant/components/tesla.py @zabuldon
homeassistant/components/*/tesla.py @zabuldon
homeassistant/components/*/tradfri.py @ggravlingen
+6 -10
View File
@@ -19,15 +19,13 @@
#
import sys
import os
from os.path import relpath
import inspect
from homeassistant.const import (__version__, __short_version__, PROJECT_NAME,
PROJECT_LONG_DESCRIPTION,
PROJECT_COPYRIGHT, PROJECT_AUTHOR,
PROJECT_GITHUB_USERNAME,
PROJECT_GITHUB_REPOSITORY,
GITHUB_PATH, GITHUB_URL)
from homeassistant.const import __version__, __short_version__
from setup import (
PROJECT_NAME, PROJECT_LONG_DESCRIPTION, PROJECT_COPYRIGHT, PROJECT_AUTHOR,
PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY, GITHUB_PATH,
GITHUB_URL)
sys.path.insert(0, os.path.abspath('_ext'))
sys.path.insert(0, os.path.abspath('../homeassistant'))
@@ -87,9 +85,7 @@ edit_on_github_src_path = 'docs/source/'
def linkcode_resolve(domain, info):
"""
Determine the URL corresponding to Python object
"""
"""Determine the URL corresponding to Python object."""
if domain != 'py':
return None
modname = info['module']
+2 -2
View File
@@ -30,8 +30,8 @@ ERROR_LOG_FILENAME = 'home-assistant.log'
DATA_LOGGING = 'logging'
FIRST_INIT_COMPONENT = set((
'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction',
'frontend', 'history'))
'system_log', 'recorder', 'mqtt', 'mqtt_eventstream', 'logger',
'introduction', 'frontend', 'history'))
def from_config_dict(config: Dict[str, Any],
+1 -1
View File
@@ -21,7 +21,7 @@ from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
from requests.exceptions import HTTPError, ConnectTimeout
REQUIREMENTS = ['abodepy==0.12.1']
REQUIREMENTS = ['abodepy==0.12.2']
_LOGGER = logging.getLogger(__name__)
@@ -34,10 +34,8 @@ def async_setup_platform(hass, config, async_add_devices,
discovery_info[ATTR_DISCOVER_AREAS] is None):
return
devices = [SpcAlarm(hass=hass,
area_id=area['id'],
name=area['name'],
state=_get_alarm_state(area['mode']))
api = hass.data[DATA_API]
devices = [SpcAlarm(api, area)
for area in discovery_info[ATTR_DISCOVER_AREAS]]
async_add_devices(devices)
@@ -46,21 +44,29 @@ def async_setup_platform(hass, config, async_add_devices,
class SpcAlarm(alarm.AlarmControlPanel):
"""Represents the SPC alarm panel."""
def __init__(self, hass, area_id, name, state):
def __init__(self, api, area):
"""Initialize the SPC alarm panel."""
self._hass = hass
self._area_id = area_id
self._name = name
self._state = state
self._api = hass.data[DATA_API]
hass.data[DATA_REGISTRY].register_alarm_device(area_id, self)
self._area_id = area['id']
self._name = area['name']
self._state = _get_alarm_state(area['mode'])
if self._state == STATE_ALARM_DISARMED:
self._changed_by = area.get('last_unset_user_name', 'unknown')
else:
self._changed_by = area.get('last_set_user_name', 'unknown')
self._api = api
@asyncio.coroutine
def async_update_from_spc(self, state):
def async_added_to_hass(self):
"""Calbback for init handlers."""
self.hass.data[DATA_REGISTRY].register_alarm_device(
self._area_id, self)
@asyncio.coroutine
def async_update_from_spc(self, state, extra):
"""Update the alarm panel with a new state."""
self._state = state
yield from self.async_update_ha_state()
self._changed_by = extra.get('changed_by', 'unknown')
self.async_schedule_update_ha_state()
@property
def should_poll(self):
@@ -72,6 +78,11 @@ class SpcAlarm(alarm.AlarmControlPanel):
"""Return the name of the device."""
return self._name
@property
def changed_by(self):
"""Return the user the last change was triggered by."""
return self._changed_by
@property
def state(self):
"""Return the state of the device."""
@@ -16,7 +16,7 @@ from homeassistant.const import (
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME)
REQUIREMENTS = ['total_connect_client==0.12']
REQUIREMENTS = ['total_connect_client==0.13']
_LOGGER = logging.getLogger(__name__)
+2
View File
@@ -15,4 +15,6 @@ ATTR_STREAM_URL = 'streamUrl'
ATTR_MAIN_TEXT = 'mainText'
ATTR_REDIRECTION_URL = 'redirectionURL'
SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH'
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
+47 -4
View File
@@ -3,6 +3,7 @@ Support for Alexa skill service end point.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/alexa/
"""
import asyncio
import enum
@@ -13,7 +14,7 @@ from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import intent
from homeassistant.components import http
from .const import DOMAIN
from .const import DOMAIN, SYN_RESOLUTION_MATCH
INTENTS_API_ENDPOINT = '/api/alexa'
@@ -123,6 +124,43 @@ class AlexaIntentsView(http.HomeAssistantView):
return self.json(alexa_response)
def resolve_slot_synonyms(key, request):
"""Check slot request for synonym resolutions."""
# Default to the spoken slot value if more than one or none are found. For
# reference to the request object structure, see the Alexa docs:
# https://tinyurl.com/ybvm7jhs
resolved_value = request['value']
if ('resolutions' in request and
'resolutionsPerAuthority' in request['resolutions'] and
len(request['resolutions']['resolutionsPerAuthority']) >= 1):
# Extract all of the possible values from each authority with a
# successful match
possible_values = []
for entry in request['resolutions']['resolutionsPerAuthority']:
if entry['status']['code'] != SYN_RESOLUTION_MATCH:
continue
possible_values.extend([item['value']['name']
for item
in entry['values']])
# If there is only one match use the resolved value, otherwise the
# resolution cannot be determined, so use the spoken slot value
if len(possible_values) == 1:
resolved_value = possible_values[0]
else:
_LOGGER.debug(
'Found multiple synonym resolutions for slot value: {%s: %s}',
key,
request['value']
)
return resolved_value
class AlexaResponse(object):
"""Help generating the response for Alexa."""
@@ -135,12 +173,17 @@ class AlexaResponse(object):
self.session_attributes = {}
self.should_end_session = True
self.variables = {}
# Intent is None if request was a LaunchRequest or SessionEndedRequest
if intent_info is not None:
for key, value in intent_info.get('slots', {}).items():
if 'value' in value:
underscored_key = key.replace('.', '_')
self.variables[underscored_key] = value['value']
# Only include slots with values
if 'value' not in value:
continue
_key = key.replace('.', '_')
self.variables[_key] = resolve_slot_synonyms(key, value)
def add_card(self, card_type, title, content):
"""Add a card to the response."""
+355 -23
View File
@@ -1,12 +1,20 @@
"""Support for alexa Smart Home Skill API."""
import asyncio
from collections import namedtuple
import logging
import math
from uuid import uuid4
import homeassistant.core as ha
from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
from homeassistant.components import switch, light
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_LOCK,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_UNLOCK, SERVICE_VOLUME_SET)
from homeassistant.components import (
alert, automation, cover, fan, group, input_boolean, light, lock,
media_player, scene, script, switch)
import homeassistant.util.color as color_util
from homeassistant.util.decorator import Registry
@@ -14,14 +22,32 @@ HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__)
API_DIRECTIVE = 'directive'
API_ENDPOINT = 'endpoint'
API_EVENT = 'event'
API_HEADER = 'header'
API_PAYLOAD = 'payload'
API_ENDPOINT = 'endpoint'
ATTR_ALEXA_DESCRIPTION = 'alexa_description'
ATTR_ALEXA_DISPLAY_CATEGORIES = 'alexa_display_categories'
ATTR_ALEXA_HIDDEN = 'alexa_hidden'
ATTR_ALEXA_NAME = 'alexa_name'
MAPPING_COMPONENT = {
switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None],
alert.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
automation.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
cover.DOMAIN: [
'DOOR', ('Alexa.PowerController',), {
cover.SUPPORT_SET_POSITION: 'Alexa.PercentageController',
}
],
fan.DOMAIN: [
'OTHER', ('Alexa.PowerController',), {
fan.SUPPORT_SET_SPEED: 'Alexa.PercentageController',
}
],
group.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
input_boolean.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
light.DOMAIN: [
'LIGHT', ('Alexa.PowerController',), {
light.SUPPORT_BRIGHTNESS: 'Alexa.BrightnessController',
@@ -30,11 +56,28 @@ MAPPING_COMPONENT = {
light.SUPPORT_COLOR_TEMP: 'Alexa.ColorTemperatureController',
}
],
lock.DOMAIN: ['SMARTLOCK', ('Alexa.LockController',), None],
media_player.DOMAIN: [
'TV', ('Alexa.PowerController',), {
media_player.SUPPORT_VOLUME_SET: 'Alexa.Speaker',
media_player.SUPPORT_PLAY: 'Alexa.PlaybackController',
media_player.SUPPORT_PAUSE: 'Alexa.PlaybackController',
media_player.SUPPORT_STOP: 'Alexa.PlaybackController',
media_player.SUPPORT_NEXT_TRACK: 'Alexa.PlaybackController',
media_player.SUPPORT_PREVIOUS_TRACK: 'Alexa.PlaybackController',
}
],
scene.DOMAIN: ['ACTIVITY_TRIGGER', ('Alexa.SceneController',), None],
script.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None],
}
Config = namedtuple('AlexaConfig', 'filter')
@asyncio.coroutine
def async_handle_message(hass, message):
def async_handle_message(hass, config, message):
"""Handle incoming API messages."""
assert message[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3'
@@ -50,7 +93,7 @@ def async_handle_message(hass, message):
"Unsupported API request %s/%s", namespace, name)
return api_error(message)
return (yield from funct_ref(hass, message))
return (yield from funct_ref(hass, config, message))
def api_message(request, name='Response', namespace='Alexa', payload=None):
@@ -99,7 +142,7 @@ def api_error(request, error_type='INTERNAL_ERROR', error_message=""):
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
@asyncio.coroutine
def async_api_discovery(hass, request):
def async_api_discovery(hass, config, request):
"""Create a API formatted discovery response.
Async friendly.
@@ -107,18 +150,40 @@ def async_api_discovery(hass, request):
discovery_endpoints = []
for entity in hass.states.async_all():
if not config.filter(entity.entity_id):
_LOGGER.debug("Not exposing %s because filtered by config",
entity.entity_id)
continue
if entity.attributes.get(ATTR_ALEXA_HIDDEN, False):
_LOGGER.debug("Not exposing %s because alexa_hidden is true",
entity.entity_id)
continue
class_data = MAPPING_COMPONENT.get(entity.domain)
if not class_data:
continue
friendly_name = entity.attributes.get(ATTR_ALEXA_NAME, entity.name)
description = entity.attributes.get(ATTR_ALEXA_DESCRIPTION,
entity.entity_id)
# Required description as per Amazon Scene docs
if entity.domain == scene.DOMAIN:
scene_fmt = '%s (Scene connected via Home Assistant)'
description = scene_fmt.format(description)
cat_key = ATTR_ALEXA_DISPLAY_CATEGORIES
display_categories = entity.attributes.get(cat_key, class_data[0])
endpoint = {
'displayCategories': [class_data[0]],
'displayCategories': [display_categories],
'additionalApplianceDetails': {},
'endpointId': entity.entity_id.replace('.', '#'),
'friendlyName': entity.name,
'description': '',
'manufacturerName': 'Unknown',
'friendlyName': friendly_name,
'description': description,
'manufacturerName': 'Home Assistant',
}
actions = set()
@@ -153,7 +218,7 @@ def async_api_discovery(hass, request):
def extract_entity(funct):
"""Decorator for extract entity object from request."""
@asyncio.coroutine
def async_api_entity_wrapper(hass, request):
def async_api_entity_wrapper(hass, config, request):
"""Process a turn on request."""
entity_id = request[API_ENDPOINT]['endpointId'].replace('#', '.')
@@ -164,7 +229,7 @@ def extract_entity(funct):
request[API_HEADER]['name'], entity_id)
return api_error(request, error_type='NO_SUCH_ENDPOINT')
return (yield from funct(hass, request, entity))
return (yield from funct(hass, config, request, entity))
return async_api_entity_wrapper
@@ -172,9 +237,13 @@ def extract_entity(funct):
@HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
@extract_entity
@asyncio.coroutine
def async_api_turn_on(hass, request, entity):
def async_api_turn_on(hass, config, request, entity):
"""Process a turn on request."""
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
domain = entity.domain
if entity.domain == group.DOMAIN:
domain = ha.DOMAIN
yield from hass.services.async_call(domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=True)
@@ -184,9 +253,13 @@ def async_api_turn_on(hass, request, entity):
@HANDLERS.register(('Alexa.PowerController', 'TurnOff'))
@extract_entity
@asyncio.coroutine
def async_api_turn_off(hass, request, entity):
def async_api_turn_off(hass, config, request, entity):
"""Process a turn off request."""
yield from hass.services.async_call(entity.domain, SERVICE_TURN_OFF, {
domain = entity.domain
if entity.domain == group.DOMAIN:
domain = ha.DOMAIN
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=True)
@@ -196,7 +269,7 @@ def async_api_turn_off(hass, request, entity):
@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness'))
@extract_entity
@asyncio.coroutine
def async_api_set_brightness(hass, request, entity):
def async_api_set_brightness(hass, config, request, entity):
"""Process a set brightness request."""
brightness = int(request[API_PAYLOAD]['brightness'])
@@ -211,7 +284,7 @@ def async_api_set_brightness(hass, request, entity):
@HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness'))
@extract_entity
@asyncio.coroutine
def async_api_adjust_brightness(hass, request, entity):
def async_api_adjust_brightness(hass, config, request, entity):
"""Process a adjust brightness request."""
brightness_delta = int(request[API_PAYLOAD]['brightnessDelta'])
@@ -235,7 +308,7 @@ def async_api_adjust_brightness(hass, request, entity):
@HANDLERS.register(('Alexa.ColorController', 'SetColor'))
@extract_entity
@asyncio.coroutine
def async_api_set_color(hass, request, entity):
def async_api_set_color(hass, config, request, entity):
"""Process a set color request."""
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES)
rgb = color_util.color_hsb_to_RGB(
@@ -263,7 +336,7 @@ def async_api_set_color(hass, request, entity):
@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature'))
@extract_entity
@asyncio.coroutine
def async_api_set_color_temperature(hass, request, entity):
def async_api_set_color_temperature(hass, config, request, entity):
"""Process a set color temperature request."""
kelvin = int(request[API_PAYLOAD]['colorTemperatureInKelvin'])
@@ -279,7 +352,7 @@ def async_api_set_color_temperature(hass, request, entity):
('Alexa.ColorTemperatureController', 'DecreaseColorTemperature'))
@extract_entity
@asyncio.coroutine
def async_api_decrease_color_temp(hass, request, entity):
def async_api_decrease_color_temp(hass, config, request, entity):
"""Process a decrease color temperature request."""
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS))
@@ -297,7 +370,7 @@ def async_api_decrease_color_temp(hass, request, entity):
('Alexa.ColorTemperatureController', 'IncreaseColorTemperature'))
@extract_entity
@asyncio.coroutine
def async_api_increase_color_temp(hass, request, entity):
def async_api_increase_color_temp(hass, config, request, entity):
"""Process a increase color temperature request."""
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))
@@ -309,3 +382,262 @@ def async_api_increase_color_temp(hass, request, entity):
}, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.SceneController', 'Activate'))
@extract_entity
@asyncio.coroutine
def async_api_activate(hass, config, request, entity):
"""Process a activate request."""
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage'))
@extract_entity
@asyncio.coroutine
def async_api_set_percentage(hass, config, request, entity):
"""Process a set percentage request."""
percentage = int(request[API_PAYLOAD]['percentage'])
service = None
data = {ATTR_ENTITY_ID: entity.entity_id}
if entity.domain == fan.DOMAIN:
service = fan.SERVICE_SET_SPEED
speed = "off"
if percentage <= 33:
speed = "low"
elif percentage <= 66:
speed = "medium"
elif percentage <= 100:
speed = "high"
data[fan.ATTR_SPEED] = speed
elif entity.domain == cover.DOMAIN:
service = SERVICE_SET_COVER_POSITION
data[cover.ATTR_POSITION] = percentage
yield from hass.services.async_call(entity.domain, service,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage'))
@extract_entity
@asyncio.coroutine
def async_api_adjust_percentage(hass, config, request, entity):
"""Process a adjust percentage request."""
percentage_delta = int(request[API_PAYLOAD]['percentageDelta'])
service = None
data = {ATTR_ENTITY_ID: entity.entity_id}
if entity.domain == fan.DOMAIN:
service = fan.SERVICE_SET_SPEED
speed = entity.attributes.get(fan.ATTR_SPEED)
if speed == "off":
current = 0
elif speed == "low":
current = 33
elif speed == "medium":
current = 66
elif speed == "high":
current = 100
# set percentage
percentage = max(0, percentage_delta + current)
speed = "off"
if percentage <= 33:
speed = "low"
elif percentage <= 66:
speed = "medium"
elif percentage <= 100:
speed = "high"
data[fan.ATTR_SPEED] = speed
elif entity.domain == cover.DOMAIN:
service = SERVICE_SET_COVER_POSITION
current = entity.attributes.get(cover.ATTR_POSITION)
data[cover.ATTR_POSITION] = max(0, percentage_delta + current)
yield from hass.services.async_call(entity.domain, service,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.LockController', 'Lock'))
@extract_entity
@asyncio.coroutine
def async_api_lock(hass, config, request, entity):
"""Process a lock request."""
yield from hass.services.async_call(entity.domain, SERVICE_LOCK, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=True)
return api_message(request)
# Not supported by Alexa yet
@HANDLERS.register(('Alexa.LockController', 'Unlock'))
@extract_entity
@asyncio.coroutine
def async_api_unlock(hass, config, request, entity):
"""Process a unlock request."""
yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.Speaker', 'SetVolume'))
@extract_entity
@asyncio.coroutine
def async_api_set_volume(hass, config, request, entity):
"""Process a set volume request."""
volume = round(float(request[API_PAYLOAD]['volume'] / 100), 2)
data = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
}
yield from hass.services.async_call(entity.domain, SERVICE_VOLUME_SET,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume'))
@extract_entity
@asyncio.coroutine
def async_api_adjust_volume(hass, config, request, entity):
"""Process a adjust volume request."""
volume_delta = int(request[API_PAYLOAD]['volume'])
current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL)
# read current state
try:
current = math.floor(int(current_level * 100))
except ZeroDivisionError:
current = 0
volume = float(max(0, volume_delta + current) / 100)
data = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
}
yield from hass.services.async_call(entity.domain,
media_player.SERVICE_VOLUME_SET,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.Speaker', 'SetMute'))
@extract_entity
@asyncio.coroutine
def async_api_set_mute(hass, config, request, entity):
"""Process a set mute request."""
mute = bool(request[API_PAYLOAD]['mute'])
data = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
}
yield from hass.services.async_call(entity.domain,
media_player.SERVICE_VOLUME_MUTE,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Play'))
@extract_entity
@asyncio.coroutine
def async_api_play(hass, config, request, entity):
"""Process a play request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PLAY,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Pause'))
@extract_entity
@asyncio.coroutine
def async_api_pause(hass, config, request, entity):
"""Process a pause request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PAUSE,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Stop'))
@extract_entity
@asyncio.coroutine
def async_api_stop(hass, config, request, entity):
"""Process a stop request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_STOP,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Next'))
@extract_entity
@asyncio.coroutine
def async_api_next(hass, config, request, entity):
"""Process a next request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(entity.domain,
SERVICE_MEDIA_NEXT_TRACK,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Previous'))
@extract_entity
@asyncio.coroutine
def async_api_previous(hass, config, request, entity):
"""Process a previous request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(entity.domain,
SERVICE_MEDIA_PREVIOUS_TRACK,
data, blocking=True)
return api_message(request)
+1 -1
View File
@@ -18,7 +18,7 @@ from homeassistant.helpers import discovery
from homeassistant.components.discovery import SERVICE_APPLE_TV
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyatv==0.3.5']
REQUIREMENTS = ['pyatv==0.3.8']
_LOGGER = logging.getLogger(__name__)
+1 -1
View File
@@ -12,7 +12,7 @@ from requests.exceptions import HTTPError, ConnectTimeout
from homeassistant.helpers import config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
REQUIREMENTS = ['pyarlo==0.0.7']
REQUIREMENTS = ['pyarlo==0.1.0']
_LOGGER = logging.getLogger(__name__)
+2 -1
View File
@@ -269,7 +269,8 @@ def setup_device(hass, config, device_config):
config)
AXIS_DEVICES[device.serial_number] = device
hass.add_job(device.start)
if event_types:
hass.add_job(device.start)
return True
@@ -30,6 +30,7 @@ DEVICE_CLASSES = [
'moving', # On means moving, Off means stopped
'occupancy', # On means occupied, Off means not occupied
'opening', # Door, window, etc.
'plug', # On means plugged in, Off means unplugged
'power', # Power, over-current, etc
'safety', # Generic on=unsafe, off=safe
'smoke', # Smoke detector
@@ -7,25 +7,32 @@ https://home-assistant.io/components/binary_sensor.aurora/
from datetime import timedelta
import logging
from aiohttp.hdrs import USER_AGENT
import requests
import voluptuous as vol
from homeassistant.components.binary_sensor \
import (BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_NAME)
from homeassistant.components.binary_sensor import (
PLATFORM_SCHEMA, BinarySensorDevice)
from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
CONF_THRESHOLD = "forecast_threshold"
_LOGGER = logging.getLogger(__name__)
CONF_ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric" \
"Administration"
CONF_THRESHOLD = 'forecast_threshold'
DEFAULT_DEVICE_CLASS = 'visible'
DEFAULT_NAME = 'Aurora Visibility'
DEFAULT_DEVICE_CLASS = "visible"
DEFAULT_THRESHOLD = 75
HA_USER_AGENT = "Home Assistant Aurora Tracker v.0.1.0"
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
URL = "http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_THRESHOLD, default=DEFAULT_THRESHOLD): cv.positive_int,
@@ -43,10 +50,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
try:
aurora_data = AuroraData(
hass.config.latitude,
hass.config.longitude,
threshold
)
hass.config.latitude, hass.config.longitude, threshold)
aurora_data.update()
except requests.exceptions.HTTPError as error:
_LOGGER.error(
@@ -85,9 +89,9 @@ class AuroraSensor(BinarySensorDevice):
attrs = {}
if self.aurora_data:
attrs["visibility_level"] = self.aurora_data.visibility_level
attrs["message"] = self.aurora_data.is_visible_text
attrs['visibility_level'] = self.aurora_data.visibility_level
attrs['message'] = self.aurora_data.is_visible_text
attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
return attrs
def update(self):
@@ -104,10 +108,7 @@ class AuroraData(object):
self.longitude = longitude
self.number_of_latitude_intervals = 513
self.number_of_longitude_intervals = 1024
self.api_url = \
"http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt"
self.headers = {"User-Agent": "Home Assistant Aurora Tracker v.0.1.0"}
self.headers = {USER_AGENT: HA_USER_AGENT}
self.threshold = int(threshold)
self.is_visible = None
self.is_visible_text = None
@@ -132,14 +133,14 @@ class AuroraData(object):
def get_aurora_forecast(self):
"""Get forecast data and parse for given long/lat."""
raw_data = requests.get(self.api_url, headers=self.headers).text
raw_data = requests.get(URL, headers=self.headers, timeout=5).text
forecast_table = [
row.strip(" ").split(" ")
for row in raw_data.split("\n")
if not row.startswith("#")
]
# convert lat and long for data points in table
# Convert lat and long for data points in table
converted_latitude = round((self.latitude / 180)
* self.number_of_latitude_intervals)
converted_longitude = round((self.longitude / 360)
@@ -27,7 +27,7 @@ SCAN_INTERVAL = timedelta(seconds=5)
# Sensor types: Name, category, device_class
SENSOR_TYPES = {
'ding': ['Ding', ['doorbell', 'stickup_cams'], 'occupancy'],
'ding': ['Ding', ['doorbell'], 'occupancy'],
'motion': ['Motion', ['doorbell', 'stickup_cams'], 'motion'],
}
@@ -67,7 +67,7 @@ class SpcBinarySensor(BinarySensorDevice):
spc_registry.register_sensor_device(zone_id, self)
@asyncio.coroutine
def async_update_from_spc(self, state):
def async_update_from_spc(self, state, extra):
"""Update the state of the device."""
self._state = state
yield from self.async_update_ha_state()
@@ -0,0 +1,103 @@
"""
Support for monitoring the state of Vultr subscriptions (VPS).
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.vultr/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.vultr import (
CONF_SUBSCRIPTION, ATTR_AUTO_BACKUPS, ATTR_ALLOWED_BANDWIDTH,
ATTR_CREATED_AT, ATTR_SUBSCRIPTION_ID, ATTR_SUBSCRIPTION_NAME,
ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, ATTR_DISK,
ATTR_COST_PER_MONTH, ATTR_OS, ATTR_REGION, ATTR_VCPUS, DATA_VULTR)
_LOGGER = logging.getLogger(__name__)
DEFAULT_DEVICE_CLASS = 'power'
DEFAULT_NAME = 'Vultr {}'
DEPENDENCIES = ['vultr']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SUBSCRIPTION): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vultr subscription (server) sensor."""
vultr = hass.data[DATA_VULTR]
subscription = config.get(CONF_SUBSCRIPTION)
name = config.get(CONF_NAME)
if subscription not in vultr.data:
_LOGGER.error("Subscription %s not found", subscription)
return False
add_devices([VultrBinarySensor(vultr, subscription, name)], True)
class VultrBinarySensor(BinarySensorDevice):
"""Representation of a Vultr subscription sensor."""
def __init__(self, vultr, subscription, name):
"""Initialize a new Vultr sensor."""
self._vultr = vultr
self._name = name
self.subscription = subscription
self.data = None
@property
def name(self):
"""Return the name of the sensor."""
try:
return self._name.format(self.data['label'])
except (KeyError, TypeError):
return self._name
@property
def icon(self):
"""Return the icon of this server."""
return 'mdi:server' if self.is_on else 'mdi:server-off'
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self.data['power_status'] == 'running'
@property
def device_class(self):
"""Return the class of this sensor."""
return DEFAULT_DEVICE_CLASS
@property
def device_state_attributes(self):
"""Return the state attributes of the Vultr subscription."""
return {
ATTR_ALLOWED_BANDWIDTH: self.data.get('allowed_bandwidth_gb'),
ATTR_AUTO_BACKUPS: self.data.get('auto_backups'),
ATTR_COST_PER_MONTH: self.data.get('cost_per_month'),
ATTR_CREATED_AT: self.data.get('date_created'),
ATTR_DISK: self.data.get('disk'),
ATTR_IPV4_ADDRESS: self.data.get('main_ip'),
ATTR_IPV6_ADDRESS: self.data.get('v6_main_ip'),
ATTR_MEMORY: self.data.get('ram'),
ATTR_OS: self.data.get('os'),
ATTR_REGION: self.data.get('location'),
ATTR_SUBSCRIPTION_ID: self.data.get('SUBID'),
ATTR_SUBSCRIPTION_NAME: self.data.get('label'),
ATTR_VCPUS: self.data.get('vcpu_count')
}
def update(self):
"""Update state of sensor."""
self._vultr.update()
self.data = self._vultr.data[self.subscription]
+4 -3
View File
@@ -4,16 +4,17 @@ Support for BloomSky weather station.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/bloomsky/
"""
import logging
from datetime import timedelta
import logging
from aiohttp.hdrs import AUTHORIZATION
import requests
import voluptuous as vol
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import discovery
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -68,7 +69,7 @@ class BloomSky(object):
"""Use the API to retrieve a list of devices."""
_LOGGER.debug("Fetching BloomSky update")
response = requests.get(
self.API_URL, headers={"Authorization": self._api_key}, timeout=10)
self.API_URL, headers={AUTHORIZATION: self._api_key}, timeout=10)
if response.status_code == 401:
raise RuntimeError("Invalid API_KEY")
elif response.status_code != 200:
+16 -19
View File
@@ -19,7 +19,7 @@ from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=10)
SCAN_INTERVAL = timedelta(seconds=90)
ARLO_MODE_ARMED = 'armed'
ARLO_MODE_DISARMED = 'disarmed'
@@ -31,6 +31,7 @@ ATTR_MOTION = 'motion_detection_sensitivity'
ATTR_POWERSAVE = 'power_save_mode'
ATTR_SIGNAL_STRENGTH = 'signal_strength'
ATTR_UNSEEN_VIDEOS = 'unseen_videos'
ATTR_LAST_REFRESH = 'last_refresh'
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
@@ -73,6 +74,8 @@ class ArloCam(Camera):
self._motion_status = False
self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
self._last_refresh = None
self._camera.base_station.refresh_rate = SCAN_INTERVAL.total_seconds()
self.attrs = {}
def camera_image(self):
@@ -105,14 +108,17 @@ class ArloCam(Camera):
def device_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_BATTERY_LEVEL: self.attrs.get(ATTR_BATTERY_LEVEL),
ATTR_BRIGHTNESS: self.attrs.get(ATTR_BRIGHTNESS),
ATTR_FLIPPED: self.attrs.get(ATTR_FLIPPED),
ATTR_MIRRORED: self.attrs.get(ATTR_MIRRORED),
ATTR_MOTION: self.attrs.get(ATTR_MOTION),
ATTR_POWERSAVE: self.attrs.get(ATTR_POWERSAVE),
ATTR_SIGNAL_STRENGTH: self.attrs.get(ATTR_SIGNAL_STRENGTH),
ATTR_UNSEEN_VIDEOS: self.attrs.get(ATTR_UNSEEN_VIDEOS),
name: value for name, value in (
(ATTR_BATTERY_LEVEL, self._camera.battery_level),
(ATTR_BRIGHTNESS, self._camera.brightness),
(ATTR_FLIPPED, self._camera.flip_state),
(ATTR_MIRRORED, self._camera.mirror_state),
(ATTR_MOTION, self._camera.motion_detection_sensitivity),
(ATTR_POWERSAVE, POWERSAVE_MODE_MAPPING.get(
self._camera.powersave_mode)),
(ATTR_SIGNAL_STRENGTH, self._camera.signal_strength),
(ATTR_UNSEEN_VIDEOS, self._camera.unseen_videos),
) if value is not None
}
@property
@@ -160,13 +166,4 @@ class ArloCam(Camera):
def update(self):
"""Add an attribute-update task to the executor pool."""
self.attrs[ATTR_BATTERY_LEVEL] = self._camera.get_battery_level
self.attrs[ATTR_BRIGHTNESS] = self._camera.get_battery_level
self.attrs[ATTR_FLIPPED] = self._camera.get_flip_state,
self.attrs[ATTR_MIRRORED] = self._camera.get_mirror_state,
self.attrs[
ATTR_MOTION] = self._camera.get_motion_detection_sensitivity,
self.attrs[ATTR_POWERSAVE] = POWERSAVE_MODE_MAPPING[
self._camera.get_powersave_mode],
self.attrs[ATTR_SIGNAL_STRENGTH] = self._camera.get_signal_strength,
self.attrs[ATTR_UNSEEN_VIDEOS] = self._camera.unseen_videos
self._camera.update()
+16 -14
View File
@@ -7,7 +7,7 @@ https://home-assistant.io/components/camera.ring/
import asyncio
import logging
from datetime import datetime, timedelta
from datetime import timedelta
import voluptuous as vol
@@ -23,6 +23,8 @@ CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
DEPENDENCIES = ['ring', 'ffmpeg']
FORCE_REFRESH_INTERVAL = timedelta(minutes=45)
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=90)
@@ -63,8 +65,8 @@ class RingCam(Camera):
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
self._last_video_id = self._camera.last_recording_id
self._video_url = self._camera.recording_url(self._last_video_id)
self._expires_at = None
self._utcnow = None
self._utcnow = dt_util.utcnow()
self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow
@property
def name(self):
@@ -123,19 +125,19 @@ class RingCam(Camera):
def update(self):
"""Update camera entity and refresh attributes."""
# extract the video expiration from URL
x_amz_expires = int(self._video_url.split('&')[0].split('=')[-1])
x_amz_date = self._video_url.split('&')[1].split('=')[-1]
_LOGGER.debug("Checking if Ring DoorBell needs to refresh video_url")
self._camera.update()
self._utcnow = dt_util.utcnow()
self._expires_at = \
timedelta(seconds=x_amz_expires) + \
dt_util.as_utc(datetime.strptime(x_amz_date, "%Y%m%dT%H%M%SZ"))
if self._last_video_id != self._camera.last_recording_id:
_LOGGER.debug("Updated Ring DoorBell last_video_id")
last_recording_id = self._camera.last_recording_id
if self._last_video_id != last_recording_id or \
self._utcnow >= self._expires_at:
_LOGGER.info("Ring DoorBell properties refreshed")
# update attributes if new video or if URL has expired
self._last_video_id = self._camera.last_recording_id
if self._utcnow >= self._expires_at:
_LOGGER.debug("Updated Ring DoorBell video_url")
self._video_url = self._camera.recording_url(self._last_video_id)
self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow
+20 -38
View File
@@ -9,12 +9,12 @@ from datetime import timedelta
import logging
import os
import functools as ft
from numbers import Number
import voluptuous as vol
from homeassistant.config import load_yaml_config_file
from homeassistant.loader import bind_hass
from homeassistant.helpers.temperature import display_temp as show_temp
from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity
@@ -22,7 +22,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
TEMP_CELSIUS)
TEMP_CELSIUS, PRECISION_WHOLE, PRECISION_TENTHS)
DOMAIN = 'climate'
@@ -71,11 +71,6 @@ ATTR_OPERATION_LIST = 'operation_list'
ATTR_SWING_MODE = 'swing_mode'
ATTR_SWING_LIST = 'swing_list'
# The degree of precision for each platform
PRECISION_WHOLE = 1
PRECISION_HALVES = 0.5
PRECISION_TENTHS = 0.1
CONVERTIBLE_ATTRIBUTE = [
ATTR_TEMPERATURE,
ATTR_TARGET_TEMP_LOW,
@@ -456,12 +451,18 @@ class ClimateDevice(Entity):
def state_attributes(self):
"""Return the optional state attributes."""
data = {
ATTR_CURRENT_TEMPERATURE:
self._convert_for_display(self.current_temperature),
ATTR_MIN_TEMP: self._convert_for_display(self.min_temp),
ATTR_MAX_TEMP: self._convert_for_display(self.max_temp),
ATTR_TEMPERATURE:
self._convert_for_display(self.target_temperature),
ATTR_CURRENT_TEMPERATURE: show_temp(
self.hass, self.current_temperature, self.temperature_unit,
self.precision),
ATTR_MIN_TEMP: show_temp(
self.hass, self.min_temp, self.temperature_unit,
self.precision),
ATTR_MAX_TEMP: show_temp(
self.hass, self.max_temp, self.temperature_unit,
self.precision),
ATTR_TEMPERATURE: show_temp(
self.hass, self.target_temperature, self.temperature_unit,
self.precision),
}
if self.target_temperature_step is not None:
@@ -469,10 +470,12 @@ class ClimateDevice(Entity):
target_temp_high = self.target_temperature_high
if target_temp_high is not None:
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display(
self.target_temperature_high)
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display(
self.target_temperature_low)
data[ATTR_TARGET_TEMP_HIGH] = show_temp(
self.hass, self.target_temperature_high, self.temperature_unit,
self.precision)
data[ATTR_TARGET_TEMP_LOW] = show_temp(
self.hass, self.target_temperature_low, self.temperature_unit,
self.precision)
humidity = self.target_humidity
if humidity is not None:
@@ -733,24 +736,3 @@ class ClimateDevice(Entity):
def max_humidity(self):
"""Return the maximum humidity."""
return 99
def _convert_for_display(self, temp):
"""Convert temperature into preferred units for display purposes."""
if temp is None:
return temp
# if the temperature is not a number this can cause issues
# with polymer components, so bail early there.
if not isinstance(temp, Number):
raise TypeError("Temperature is not a number: %s" % temp)
if self.temperature_unit != self.unit_of_measurement:
temp = convert_temperature(
temp, self.temperature_unit, self.unit_of_measurement)
# Round in the units appropriate
if self.precision == PRECISION_HALVES:
return round(temp * 2) / 2.0
elif self.precision == PRECISION_TENTHS:
return round(temp, 1)
# PRECISION_WHOLE as a fall back
return round(temp)
+12 -13
View File
@@ -9,12 +9,9 @@ import logging
import voluptuous as vol
from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, PRECISION_HALVES,
STATE_AUTO, STATE_ON, STATE_OFF,
)
STATE_ON, STATE_OFF, STATE_AUTO, PLATFORM_SCHEMA, ClimateDevice)
from homeassistant.const import (
CONF_MAC, TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE)
CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-eq3bt==0.1.6']
@@ -58,15 +55,17 @@ class EQ3BTSmartThermostat(ClimateDevice):
def __init__(self, _mac, _name):
"""Initialize the thermostat."""
# we want to avoid name clash with this module..
# We want to avoid name clash with this module.
import eq3bt as eq3
self.modes = {eq3.Mode.Open: STATE_ON,
eq3.Mode.Closed: STATE_OFF,
eq3.Mode.Auto: STATE_AUTO,
eq3.Mode.Manual: STATE_MANUAL,
eq3.Mode.Boost: STATE_BOOST,
eq3.Mode.Away: STATE_AWAY}
self.modes = {
eq3.Mode.Open: STATE_ON,
eq3.Mode.Closed: STATE_OFF,
eq3.Mode.Auto: STATE_AUTO,
eq3.Mode.Manual: STATE_MANUAL,
eq3.Mode.Boost: STATE_BOOST,
eq3.Mode.Away: STATE_AWAY,
}
self.reverse_modes = {v: k for k, v in self.modes.items()}
@@ -153,11 +152,11 @@ class EQ3BTSmartThermostat(ClimateDevice):
def device_state_attributes(self):
"""Return the device specific state attributes."""
dev_specific = {
ATTR_STATE_AWAY_END: self._thermostat.away_end,
ATTR_STATE_LOCKED: self._thermostat.locked,
ATTR_STATE_LOW_BAT: self._thermostat.low_battery,
ATTR_STATE_VALVE: self._thermostat.valve_state,
ATTR_STATE_WINDOW_OPEN: self._thermostat.window_open,
ATTR_STATE_AWAY_END: self._thermostat.away_end,
}
return dev_specific
@@ -163,6 +163,7 @@ class GenericThermostat(ClimateDevice):
"""Set operation mode."""
if operation_mode == STATE_AUTO:
self._enabled = True
self._async_control_heating()
elif operation_mode == STATE_OFF:
self._enabled = False
if self._is_device_active:
@@ -7,7 +7,6 @@ https://home-assistant.io/components/climate.homematic/
import logging
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
from homeassistant.util.temperature import convert
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
DEPENDENCIES = ['homematic']
@@ -121,12 +120,12 @@ class HMThermostat(HMDevice, ClimateDevice):
@property
def min_temp(self):
"""Return the minimum temperature - 4.5 means off."""
return convert(4.5, TEMP_CELSIUS, self.unit_of_measurement)
return 4.5
@property
def max_temp(self):
"""Return the maximum temperature - 30.5 means on."""
return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement)
return 30.5
def _init_data_struct(self):
"""Generate a data dict (self._data) from the Homematic metadata."""
+33 -22
View File
@@ -13,9 +13,11 @@ from homeassistant.const import CONF_NAME, TEMP_CELSIUS, ATTR_TEMPERATURE
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
CONF_SETPOINT_ADDRESS = 'setpoint_address'
CONF_SETPOINT_SHIFT_ADDRESS = 'setpoint_shift_address'
CONF_SETPOINT_SHIFT_STATE_ADDRESS = 'setpoint_shift_state_address'
CONF_SETPOINT_SHIFT_STEP = 'setpoint_shift_step'
CONF_SETPOINT_SHIFT_MAX = 'setpoint_shift_max'
CONF_SETPOINT_SHIFT_MIN = 'setpoint_shift_min'
CONF_TEMPERATURE_ADDRESS = 'temperature_address'
CONF_TARGET_TEMPERATURE_ADDRESS = 'target_temperature_address'
CONF_OPERATION_MODE_ADDRESS = 'operation_mode_address'
@@ -28,15 +30,24 @@ CONF_OPERATION_MODE_NIGHT_ADDRESS = 'operation_mode_night_address'
CONF_OPERATION_MODE_COMFORT_ADDRESS = 'operation_mode_comfort_address'
DEFAULT_NAME = 'KNX Climate'
DEFAULT_SETPOINT_SHIFT_STEP = 0.5
DEFAULT_SETPOINT_SHIFT_MAX = 6
DEFAULT_SETPOINT_SHIFT_MIN = -6
DEPENDENCIES = ['knx']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_SETPOINT_ADDRESS): cv.string,
vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string,
vol.Required(CONF_TARGET_TEMPERATURE_ADDRESS): cv.string,
vol.Optional(CONF_SETPOINT_SHIFT_ADDRESS): cv.string,
vol.Optional(CONF_SETPOINT_SHIFT_STATE_ADDRESS): cv.string,
vol.Optional(CONF_SETPOINT_SHIFT_STEP,
default=DEFAULT_SETPOINT_SHIFT_STEP): vol.All(
float, vol.Range(min=0, max=2)),
vol.Optional(CONF_SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX):
vol.All(int, vol.Range(min=-32, max=0)),
vol.Optional(CONF_SETPOINT_SHIFT_MIN, default=DEFAULT_SETPOINT_SHIFT_MIN):
vol.All(int, vol.Range(min=0, max=32)),
vol.Optional(CONF_OPERATION_MODE_ADDRESS): cv.string,
vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string,
vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string,
@@ -77,6 +88,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices):
def async_add_devices_config(hass, config, async_add_devices):
"""Set up climate for KNX platform configured within plattform."""
import xknx
climate = xknx.devices.Climate(
hass.data[DATA_KNX].xknx,
name=config.get(CONF_NAME),
@@ -84,12 +96,16 @@ def async_add_devices_config(hass, config, async_add_devices):
CONF_TEMPERATURE_ADDRESS),
group_address_target_temperature=config.get(
CONF_TARGET_TEMPERATURE_ADDRESS),
group_address_setpoint=config.get(
CONF_SETPOINT_ADDRESS),
group_address_setpoint_shift=config.get(
CONF_SETPOINT_SHIFT_ADDRESS),
group_address_setpoint_shift_state=config.get(
CONF_SETPOINT_SHIFT_STATE_ADDRESS),
setpoint_shift_step=config.get(
CONF_SETPOINT_SHIFT_STEP),
setpoint_shift_max=config.get(
CONF_SETPOINT_SHIFT_MAX),
setpoint_shift_min=config.get(
CONF_SETPOINT_SHIFT_MIN),
group_address_operation_mode=config.get(
CONF_OPERATION_MODE_ADDRESS),
group_address_operation_mode_state=config.get(
@@ -118,8 +134,6 @@ class KNXClimate(ClimateDevice):
self.async_register_callbacks()
self._unit_of_measurement = TEMP_CELSIUS
self._away = False # not yet supported
self._is_fan_on = False # not yet supported
def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed."""
@@ -150,28 +164,25 @@ class KNXClimate(ClimateDevice):
"""Return the current temperature."""
return self.device.temperature.value
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
return self.device.setpoint_shift_step
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self.device.target_temperature_comfort
return self.device.target_temperature.value
@property
def target_temperature_high(self):
"""Return the highbound target temperature we try to reach."""
if self.device.target_temperature_comfort:
return max(
self.device.target_temperature_comfort,
self.device.target_temperature.value)
return None
def min_temp(self):
"""Return the minimum temperature."""
return self.device.target_temperature_min
@property
def target_temperature_low(self):
"""Return the lowbound target temperature we try to reach."""
if self.device.target_temperature_comfort:
return min(
self.device.target_temperature_comfort,
self.device.target_temperature.value)
return None
def max_temp(self):
"""Return the maximum temperature."""
return self.device.target_temperature_max
@asyncio.coroutine
def async_set_temperature(self, **kwargs):
@@ -179,7 +190,7 @@ class KNXClimate(ClimateDevice):
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None:
return
yield from self.device.set_target_temperature_comfort(temperature)
yield from self.device.set_target_temperature(temperature)
yield from self.async_update_ha_state()
@property
+78 -56
View File
@@ -4,46 +4,51 @@ Support for Wink thermostats, Air Conditioners, and Water Heaters.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.wink/
"""
import logging
import asyncio
import logging
from homeassistant.components.wink import WinkDevice, DOMAIN
from homeassistant.components.climate import (
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
ATTR_TEMPERATURE, STATE_FAN_ONLY,
ATTR_CURRENT_HUMIDITY, STATE_ECO, STATE_ELECTRIC,
STATE_PERFORMANCE, STATE_HIGH_DEMAND,
STATE_HEAT_PUMP, STATE_GAS)
STATE_ECO, STATE_GAS, STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ELECTRIC,
STATE_FAN_ONLY, STATE_HEAT_PUMP, ATTR_TEMPERATURE, STATE_HIGH_DEMAND,
STATE_PERFORMANCE, ATTR_TARGET_TEMP_LOW, ATTR_CURRENT_HUMIDITY,
ATTR_TARGET_TEMP_HIGH, ClimateDevice)
from homeassistant.components.wink import DOMAIN, WinkDevice
from homeassistant.const import (
TEMP_CELSIUS, STATE_ON,
STATE_OFF, STATE_UNKNOWN)
STATE_ON, STATE_OFF, TEMP_CELSIUS, STATE_UNKNOWN, PRECISION_TENTHS)
from homeassistant.helpers.temperature import display_temp as show_temp
_LOGGER = logging.getLogger(__name__)
ATTR_ECO_TARGET = 'eco_target'
ATTR_EXTERNAL_TEMPERATURE = 'external_temperature'
ATTR_OCCUPIED = 'occupied'
ATTR_RHEEM_TYPE = 'rheem_type'
ATTR_SCHEDULE_ENABLED = 'schedule_enabled'
ATTR_SMART_TEMPERATURE = 'smart_temperature'
ATTR_TOTAL_CONSUMPTION = 'total_consumption'
ATTR_VACATION_MODE = 'vacation_mode'
DEPENDENCIES = ['wink']
SPEED_LOW = 'low'
SPEED_MEDIUM = 'medium'
SPEED_HIGH = 'high'
HA_STATE_TO_WINK = {STATE_AUTO: 'auto',
STATE_ECO: 'eco',
STATE_FAN_ONLY: 'fan_only',
STATE_HEAT: 'heat_only',
STATE_COOL: 'cool_only',
STATE_PERFORMANCE: 'performance',
STATE_HIGH_DEMAND: 'high_demand',
STATE_HEAT_PUMP: 'heat_pump',
STATE_ELECTRIC: 'electric_only',
STATE_GAS: 'gas',
STATE_OFF: 'off'}
WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()}
HA_STATE_TO_WINK = {
STATE_AUTO: 'auto',
STATE_COOL: 'cool_only',
STATE_ECO: 'eco',
STATE_ELECTRIC: 'electric_only',
STATE_FAN_ONLY: 'fan_only',
STATE_GAS: 'gas',
STATE_HEAT: 'heat_only',
STATE_HEAT_PUMP: 'heat_pump',
STATE_HIGH_DEMAND: 'high_demand',
STATE_OFF: 'off',
STATE_PERFORMANCE: 'performance',
}
ATTR_EXTERNAL_TEMPERATURE = "external_temperature"
ATTR_SMART_TEMPERATURE = "smart_temperature"
ATTR_ECO_TARGET = "eco_target"
ATTR_OCCUPIED = "occupied"
WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()}
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -85,15 +90,18 @@ class WinkThermostat(WinkDevice, ClimateDevice):
target_temp_high = self.target_temperature_high
target_temp_low = self.target_temperature_low
if target_temp_high is not None:
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display(
self.target_temperature_high)
data[ATTR_TARGET_TEMP_HIGH] = show_temp(
self.hass, self.target_temperature_high, self.temperature_unit,
PRECISION_TENTHS)
if target_temp_low is not None:
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display(
self.target_temperature_low)
data[ATTR_TARGET_TEMP_LOW] = show_temp(
self.hass, self.target_temperature_low, self.temperature_unit,
PRECISION_TENTHS)
if self.external_temperature:
data[ATTR_EXTERNAL_TEMPERATURE] = self._convert_for_display(
self.external_temperature)
data[ATTR_EXTERNAL_TEMPERATURE] = show_temp(
self.hass, self.external_temperature, self.temperature_unit,
PRECISION_TENTHS)
if self.smart_temperature:
data[ATTR_SMART_TEMPERATURE] = self.smart_temperature
@@ -139,7 +147,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
@property
def eco_target(self):
"""Return status of eco target (Is the termostat in eco mode)."""
"""Return status of eco target (Is the thermostat in eco mode)."""
return self.wink.eco_target()
@property
@@ -249,7 +257,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
if ha_mode is not None:
op_list.append(ha_mode)
else:
error = "Invaid operation mode mapping. " + mode + \
error = "Invalid operation mode mapping. " + mode + \
" doesn't map. Please report this."
_LOGGER.error(error)
return op_list
@@ -297,7 +305,6 @@ class WinkThermostat(WinkDevice, ClimateDevice):
minimum = 7 # Default minimum
min_min = self.wink.min_min_set_point()
min_max = self.wink.min_max_set_point()
return_value = minimum
if self.current_operation == STATE_HEAT:
if min_min:
return_value = min_min
@@ -323,7 +330,6 @@ class WinkThermostat(WinkDevice, ClimateDevice):
maximum = 35 # Default maximum
max_min = self.wink.max_min_set_point()
max_max = self.wink.max_max_set_point()
return_value = maximum
if self.current_operation == STATE_HEAT:
if max_min:
return_value = max_min
@@ -360,13 +366,15 @@ class WinkAC(WinkDevice, ClimateDevice):
target_temp_high = self.target_temperature_high
target_temp_low = self.target_temperature_low
if target_temp_high is not None:
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display(
self.target_temperature_high)
data[ATTR_TARGET_TEMP_HIGH] = show_temp(
self.hass, self.target_temperature_high, self.temperature_unit,
PRECISION_TENTHS)
if target_temp_low is not None:
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display(
self.target_temperature_low)
data["total_consumption"] = self.wink.total_consumption()
data["schedule_enabled"] = self.wink.schedule_enabled()
data[ATTR_TARGET_TEMP_LOW] = show_temp(
self.hass, self.target_temperature_low, self.temperature_unit,
PRECISION_TENTHS)
data[ATTR_TOTAL_CONSUMPTION] = self.wink.total_consumption()
data[ATTR_SCHEDULE_ENABLED] = self.wink.schedule_enabled()
return data
@@ -377,11 +385,14 @@ class WinkAC(WinkDevice, ClimateDevice):
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
"""Return current operation ie. auto_eco, cool_only, fan_only."""
if not self.wink.is_on():
current_op = STATE_OFF
else:
current_op = WINK_STATE_TO_HA.get(self.wink.current_hvac_mode())
wink_mode = self.wink.current_mode()
if wink_mode == "auto_eco":
wink_mode = "eco"
current_op = WINK_STATE_TO_HA.get(wink_mode)
if current_op is None:
current_op = STATE_UNKNOWN
return current_op
@@ -392,11 +403,13 @@ class WinkAC(WinkDevice, ClimateDevice):
op_list = ['off']
modes = self.wink.modes()
for mode in modes:
if mode == "auto_eco":
mode = "eco"
ha_mode = WINK_STATE_TO_HA.get(mode)
if ha_mode is not None:
op_list.append(ha_mode)
else:
error = "Invaid operation mode mapping. " + mode + \
error = "Invalid operation mode mapping. " + mode + \
" doesn't map. Please report this."
_LOGGER.error(error)
return op_list
@@ -420,15 +433,19 @@ class WinkAC(WinkDevice, ClimateDevice):
@property
def current_fan_mode(self):
"""Return the current fan mode."""
"""
Return the current fan mode.
The official Wink app only supports 3 modes [low, medium, high]
which are equal to [0.33, 0.66, 1.0] respectively.
"""
speed = self.wink.current_fan_speed()
if speed <= 0.4 and speed > 0.3:
if speed <= 0.33:
return SPEED_LOW
elif speed <= 0.8 and speed > 0.5:
elif speed <= 0.66:
return SPEED_MEDIUM
elif speed <= 1.0 and speed > 0.8:
else:
return SPEED_HIGH
return STATE_UNKNOWN
@property
def fan_list(self):
@@ -436,11 +453,16 @@ class WinkAC(WinkDevice, ClimateDevice):
return [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
def set_fan_mode(self, fan):
"""Set fan speed."""
"""
Set fan speed.
The official Wink app only supports 3 modes [low, medium, high]
which are equal to [0.33, 0.66, 1.0] respectively.
"""
if fan == SPEED_LOW:
speed = 0.4
speed = 0.33
elif fan == SPEED_MEDIUM:
speed = 0.8
speed = 0.66
elif fan == SPEED_HIGH:
speed = 1.0
self.wink.set_ac_fan_speed(speed)
@@ -459,8 +481,8 @@ class WinkWaterHeater(WinkDevice, ClimateDevice):
def device_state_attributes(self):
"""Return the optional state attributes."""
data = {}
data["vacation_mode"] = self.wink.vacation_mode_enabled()
data["rheem_type"] = self.wink.rheem_type()
data[ATTR_VACATION_MODE] = self.wink.vacation_mode_enabled()
data[ATTR_RHEEM_TYPE] = self.wink.rheem_type()
return data
@@ -492,7 +514,7 @@ class WinkWaterHeater(WinkDevice, ClimateDevice):
if ha_mode is not None:
op_list.append(ha_mode)
else:
error = "Invaid operation mode mapping. " + mode + \
error = "Invalid operation mode mapping. " + mode + \
" doesn't map. Please report this."
_LOGGER.error(error)
return op_list
+48 -8
View File
@@ -1,5 +1,6 @@
"""Component to integrate the Home Assistant cloud."""
import asyncio
from datetime import datetime
import json
import logging
import os
@@ -8,6 +9,9 @@ import voluptuous as vol
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE)
from homeassistant.helpers import entityfilter
from homeassistant.util import dt as dt_util
from homeassistant.components.alexa import smart_home
from . import http_api, iot
from .const import CONFIG_DIR, DOMAIN, SERVERS
@@ -16,6 +20,8 @@ REQUIREMENTS = ['warrant==0.5.0']
_LOGGER = logging.getLogger(__name__)
CONF_ALEXA = 'alexa'
CONF_ALEXA_FILTER = 'filter'
CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id'
@@ -24,6 +30,13 @@ MODE_DEV = 'development'
DEFAULT_MODE = MODE_DEV
DEPENDENCIES = ['http']
ALEXA_SCHEMA = vol.Schema({
vol.Optional(
CONF_ALEXA_FILTER,
default=lambda: entityfilter.generate_filter([], [], [], [])
): entityfilter.FILTER_SCHEMA,
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
@@ -33,6 +46,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_USER_POOL_ID): str,
vol.Required(CONF_REGION): str,
vol.Required(CONF_RELAYER): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA
}),
}, extra=vol.ALLOW_EXTRA)
@@ -45,6 +59,10 @@ def async_setup(hass, config):
else:
kwargs = {CONF_MODE: DEFAULT_MODE}
if CONF_ALEXA not in kwargs:
kwargs[CONF_ALEXA] = ALEXA_SCHEMA({})
kwargs[CONF_ALEXA] = smart_home.Config(**kwargs[CONF_ALEXA])
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
@asyncio.coroutine
@@ -62,11 +80,11 @@ class Cloud:
"""Store the configuration of the cloud connection."""
def __init__(self, hass, mode, cognito_client_id=None, user_pool_id=None,
region=None, relayer=None):
region=None, relayer=None, alexa=None):
"""Create an instance of Cloud."""
self.hass = hass
self.mode = mode
self.email = None
self.alexa_config = alexa
self.id_token = None
self.access_token = None
self.refresh_token = None
@@ -89,7 +107,29 @@ class Cloud:
@property
def is_logged_in(self):
"""Get if cloud is logged in."""
return self.email is not None
return self.id_token is not None
@property
def subscription_expired(self):
"""Return a boolen if the subscription has expired."""
# For now, don't enforce subscriptions to exist
if 'custom:sub-exp' not in self.claims:
return False
return dt_util.utcnow() > self.expiration_date
@property
def expiration_date(self):
"""Return the subscription expiration as a UTC datetime object."""
return datetime.combine(
dt_util.parse_date(self.claims['custom:sub-exp']),
datetime.min.time()).replace(tzinfo=dt_util.UTC)
@property
def claims(self):
"""Get the claims from the id token."""
from jose import jwt
return jwt.get_unverified_claims(self.id_token)
@property
def user_info_path(self):
@@ -110,18 +150,20 @@ class Cloud:
if os.path.isfile(user_info):
with open(user_info, 'rt') as file:
info = json.loads(file.read())
self.email = info['email']
self.id_token = info['id_token']
self.access_token = info['access_token']
self.refresh_token = info['refresh_token']
yield from self.hass.async_add_job(load_config)
if self.email is not None:
if self.id_token is not None:
yield from self.iot.connect()
def path(self, *parts):
"""Get config path inside cloud dir."""
"""Get config path inside cloud dir.
Async friendly.
"""
return self.hass.config.path(CONFIG_DIR, *parts)
@asyncio.coroutine
@@ -129,7 +171,6 @@ class Cloud:
"""Close connection and remove all credentials."""
yield from self.iot.disconnect()
self.email = None
self.id_token = None
self.access_token = None
self.refresh_token = None
@@ -141,7 +182,6 @@ class Cloud:
"""Write user info to a file."""
with open(self.user_info_path, 'wt') as file:
file.write(json.dumps({
'email': self.email,
'id_token': self.id_token,
'access_token': self.access_token,
'refresh_token': self.refresh_token,
@@ -113,7 +113,6 @@ def login(cloud, email, password):
cloud.id_token = cognito.id_token
cloud.access_token = cognito.access_token
cloud.refresh_token = cognito.refresh_token
cloud.email = email
cloud.write_user_info()
+5
View File
@@ -12,3 +12,8 @@ SERVERS = {
# 'relayer': ''
# }
}
MESSAGE_EXPIRATION = """
It looks like your Home Assistant Cloud subscription has expired. Please check
your [account page](/config/cloud/account) to continue using the service.
"""
+8 -2
View File
@@ -79,8 +79,10 @@ class CloudLoginView(HomeAssistantView):
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(auth_api.login, cloud, data['email'],
data['password'])
hass.async_add_job(cloud.iot.connect)
hass.async_add_job(cloud.iot.connect)
# Allow cloud to start connecting.
yield from asyncio.sleep(0, loop=hass.loop)
return self.json(_account_data(cloud))
@@ -222,6 +224,10 @@ class CloudConfirmForgotPasswordView(HomeAssistantView):
def _account_data(cloud):
"""Generate the auth data JSON response."""
claims = cloud.claims
return {
'email': cloud.email
'email': claims['email'],
'sub_exp': claims.get('custom:sub-exp'),
'cloud': cloud.iot.state,
}
+55 -22
View File
@@ -9,11 +9,16 @@ from homeassistant.components.alexa import smart_home
from homeassistant.util.decorator import Registry
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import auth_api
from .const import MESSAGE_EXPIRATION
HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__)
STATE_CONNECTING = 'connecting'
STATE_CONNECTED = 'connected'
STATE_DISCONNECTED = 'disconnected'
class UnknownHandler(Exception):
"""Exception raised when trying to handle unknown handler."""
@@ -25,27 +30,41 @@ class CloudIoT:
def __init__(self, cloud):
"""Initialize the CloudIoT class."""
self.cloud = cloud
# The WebSocket client
self.client = None
# Scheduled sleep task till next connection retry
self.retry_task = None
# Boolean to indicate if we wanted the connection to close
self.close_requested = False
# The current number of attempts to connect, impacts wait time
self.tries = 0
@property
def is_connected(self):
"""Return if connected to the cloud."""
return self.client is not None
# Current state of the connection
self.state = STATE_DISCONNECTED
@asyncio.coroutine
def connect(self):
"""Connect to the IoT broker."""
if self.client is not None:
raise RuntimeError('Cannot connect while already connected')
self.close_requested = False
hass = self.cloud.hass
remove_hass_stop_listener = None
if self.cloud.subscription_expired:
# Try refreshing the token to see if it is still expired.
yield from hass.async_add_job(auth_api.check_token, self.cloud)
if self.cloud.subscription_expired:
hass.components.persistent_notification.async_create(
MESSAGE_EXPIRATION, 'Subscription expired',
'cloud_subscription_expired')
self.state = STATE_DISCONNECTED
return
if self.state == STATE_CONNECTED:
raise RuntimeError('Already connected')
self.state = STATE_CONNECTING
self.close_requested = False
remove_hass_stop_listener = None
session = async_get_clientsession(self.cloud.hass)
client = None
disconnect_warn = None
@asyncio.coroutine
def _handle_hass_stop(event):
@@ -54,8 +73,6 @@ class CloudIoT:
remove_hass_stop_listener = None
yield from self.disconnect()
client = None
disconnect_warn = None
try:
yield from hass.async_add_job(auth_api.check_token, self.cloud)
@@ -70,13 +87,14 @@ class CloudIoT:
EVENT_HOMEASSISTANT_STOP, _handle_hass_stop)
_LOGGER.info('Connected')
self.state = STATE_CONNECTED
while not client.closed:
msg = yield from client.receive()
if msg.type in (WSMsgType.ERROR, WSMsgType.CLOSED,
WSMsgType.CLOSING):
disconnect_warn = 'Closed by server'
disconnect_warn = 'Connection cancelled.'
break
elif msg.type != WSMsgType.TEXT:
@@ -144,20 +162,33 @@ class CloudIoT:
self.client = None
yield from client.close()
if not self.close_requested:
if self.close_requested:
self.state = STATE_DISCONNECTED
else:
self.state = STATE_CONNECTING
self.tries += 1
# Sleep 0, 5, 10, 15 … up to 30 seconds between retries
yield from asyncio.sleep(
min(30, (self.tries - 1) * 5), loop=hass.loop)
hass.async_add_job(self.connect())
try:
# Sleep 0, 5, 10, 15 … up to 30 seconds between retries
self.retry_task = hass.async_add_job(asyncio.sleep(
min(30, (self.tries - 1) * 5), loop=hass.loop))
yield from self.retry_task
self.retry_task = None
hass.async_add_job(self.connect())
except asyncio.CancelledError:
# Happens if disconnect called
pass
@asyncio.coroutine
def disconnect(self):
"""Disconnect the client."""
self.close_requested = True
yield from self.client.close()
if self.client is not None:
yield from self.client.close()
elif self.retry_task is not None:
self.retry_task.cancel()
@asyncio.coroutine
@@ -175,7 +206,9 @@ def async_handle_message(hass, cloud, handler_name, payload):
@asyncio.coroutine
def async_handle_alexa(hass, cloud, payload):
"""Handle an incoming IoT message for Alexa."""
return (yield from smart_home.async_handle_message(hass, payload))
return (yield from smart_home.async_handle_message(hass,
cloud.alexa_config,
payload))
@HANDLERS.register('cloud')
+59 -5
View File
@@ -1,17 +1,19 @@
"""Provide configuration end points for Z-Wave."""
import asyncio
import logging
from collections import deque
from aiohttp.web import Response
import homeassistant.core as ha
from homeassistant.const import HTTP_NOT_FOUND
from homeassistant.const import HTTP_NOT_FOUND, HTTP_OK
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.config import EditKeyBasedConfigView
from homeassistant.components.zwave import const, DEVICE_CONFIG_SCHEMA_ENTRY
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONFIG_PATH = 'zwave_device_config.yaml'
OZW_LOG_FILENAME = 'OZW_Log.txt'
URL_API_OZW_LOG = '/api/zwave/ozwlog'
@asyncio.coroutine
@@ -25,12 +27,64 @@ def async_setup(hass):
hass.http.register_view(ZWaveNodeGroupView)
hass.http.register_view(ZWaveNodeConfigView)
hass.http.register_view(ZWaveUserCodeView)
hass.http.register_static_path(
URL_API_OZW_LOG, hass.config.path(OZW_LOG_FILENAME), False)
hass.http.register_view(ZWaveLogView)
hass.http.register_view(ZWaveConfigWriteView)
return True
class ZWaveLogView(HomeAssistantView):
"""View to read the ZWave log file."""
url = "/api/zwave/ozwlog"
name = "api:zwave:ozwlog"
# pylint: disable=no-self-use
@asyncio.coroutine
def get(self, request):
"""Retrieve the lines from ZWave log."""
try:
lines = int(request.query.get('lines', 0))
except ValueError:
return Response(text='Invalid datetime', status=400)
hass = request.app['hass']
response = yield from hass.async_add_job(self._get_log, hass, lines)
return Response(text='\n'.join(response))
def _get_log(self, hass, lines):
"""Retrieve the logfile content."""
logfilepath = hass.config.path(OZW_LOG_FILENAME)
with open(logfilepath, 'r') as logfile:
data = (line.rstrip() for line in logfile)
if lines == 0:
loglines = list(data)
else:
loglines = deque(data, lines)
return loglines
class ZWaveConfigWriteView(HomeAssistantView):
"""View to save the ZWave configuration to zwcfg_xxxxx.xml."""
url = "/api/zwave/saveconfig"
name = "api:zwave:saveconfig"
@ha.callback
def post(self, request):
"""Save cache configuration to zwcfg_xxxxx.xml."""
hass = request.app['hass']
network = hass.data.get(const.DATA_NETWORK)
if network is None:
return self.json_message('No Z-Wave network data found',
HTTP_NOT_FOUND)
_LOGGER.info("Z-Wave configuration written to file.")
network.write_config()
return self.json_message('Z-Wave configuration saved to file.',
HTTP_OK)
class ZWaveNodeValueView(HomeAssistantView):
"""View to return the node values."""
+3 -2
View File
@@ -207,7 +207,7 @@ class Configurator(object):
self.hass.bus.async_listen_once(EVENT_TIME_CHANGED, deferred_remove)
@async_callback
@asyncio.coroutine
def async_handle_service_call(self, call):
"""Handle a configure service call."""
request_id = call.data.get(ATTR_CONFIGURE_ID)
@@ -220,7 +220,8 @@ class Configurator(object):
# field validation goes here?
if callback:
self.hass.async_add_job(callback, call.data.get(ATTR_FIELDS, {}))
yield from self.hass.async_add_job(callback,
call.data.get(ATTR_FIELDS, {}))
def _generate_unique_id(self):
"""Generate a unique configurator ID."""
@@ -140,13 +140,13 @@ def async_setup(hass, config):
hass.services.async_register(
DOMAIN, SERVICE_INCREMENT, async_handler_service,
descriptions[DOMAIN][SERVICE_INCREMENT], SERVICE_SCHEMA)
descriptions[SERVICE_INCREMENT], SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_DECREMENT, async_handler_service,
descriptions[DOMAIN][SERVICE_DECREMENT], SERVICE_SCHEMA)
descriptions[SERVICE_DECREMENT], SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_RESET, async_handler_service,
descriptions[DOMAIN][SERVICE_RESET], SERVICE_SCHEMA)
descriptions[SERVICE_RESET], SERVICE_SCHEMA)
yield from component.async_add_entities(entities)
return True
@@ -0,0 +1,20 @@
# Describes the format for available counter services
decrement:
description: Decrement a counter.
fields:
entity_id:
description: Entity id of the counter to decrement.
example: 'counter.count0'
increment:
description: Increment a counter.
fields:
entity_id:
description: Entity id of the counter to increment.
example: 'counter.count0'
reset:
description: Reset a counter.
fields:
entity_id:
description: Entity id of the counter to reset.
example: 'counter.count0'
@@ -4,6 +4,7 @@ Support for Lutron Caseta shades.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.lutron_caseta/
"""
import asyncio
import logging
from homeassistant.components.cover import (
@@ -18,7 +19,8 @@ DEPENDENCIES = ['lutron_caseta']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Lutron Caseta shades as a cover device."""
devs = []
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
@@ -27,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev = LutronCasetaCover(cover_device, bridge)
devs.append(dev)
add_devices(devs, True)
async_add_devices(devs, True)
class LutronCasetaCover(LutronCasetaDevice, CoverDevice):
@@ -48,21 +50,25 @@ class LutronCasetaCover(LutronCasetaDevice, CoverDevice):
"""Return the current position of cover."""
return self._state['current_state']
def close_cover(self, **kwargs):
@asyncio.coroutine
def async_close_cover(self, **kwargs):
"""Close the cover."""
self._smartbridge.set_value(self._device_id, 0)
def open_cover(self, **kwargs):
@asyncio.coroutine
def async_open_cover(self, **kwargs):
"""Open the cover."""
self._smartbridge.set_value(self._device_id, 100)
def set_cover_position(self, **kwargs):
@asyncio.coroutine
def async_set_cover_position(self, **kwargs):
"""Move the shade to a specific position."""
if ATTR_POSITION in kwargs:
position = kwargs[ATTR_POSITION]
self._smartbridge.set_value(self._device_id, position)
def update(self):
@asyncio.coroutine
def async_update(self):
"""Call when forcing a refresh of the device."""
self._state = self._smartbridge.get_device_by_id(self._device_id)
_LOGGER.debug(self._state)
+3
View File
@@ -104,6 +104,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the MQTT Cover."""
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = hass
@@ -76,6 +76,7 @@ ATTR_LOCATION_NAME = 'location_name'
ATTR_MAC = 'mac'
ATTR_NAME = 'name'
ATTR_SOURCE_TYPE = 'source_type'
ATTR_VENDOR = 'vendor'
SOURCE_TYPE_GPS = 'gps'
SOURCE_TYPE_ROUTER = 'router'
@@ -285,11 +286,6 @@ class DeviceTracker(object):
if device.track:
yield from device.async_update_ha_state()
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
ATTR_ENTITY_ID: device.entity_id,
ATTR_HOST_NAME: device.host_name,
})
# During init, we ignore the group
if self.group and self.track_new:
self.group.async_set_group(
@@ -299,6 +295,13 @@ class DeviceTracker(object):
# lookup mac vendor string to be stored in config
yield from device.set_vendor_for_mac()
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
ATTR_ENTITY_ID: device.entity_id,
ATTR_HOST_NAME: device.host_name,
ATTR_MAC: device.mac,
ATTR_VENDOR: device.vendor,
})
# update known_devices.yaml
self.hass.async_add_job(
self.async_update_config(
@@ -0,0 +1,138 @@
"""
Support for the Hitron CODA-4582U, provided by Rogers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.hitron_coda/
"""
import logging
from collections import namedtuple
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME
)
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
def get_scanner(_hass, config):
"""Validate the configuration and return a Nmap scanner."""
scanner = HitronCODADeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
Device = namedtuple('Device', ['mac', 'name'])
class HitronCODADeviceScanner(DeviceScanner):
"""This class scans for devices using the CODA's web interface."""
def __init__(self, config):
"""Initialize the scanner."""
self.last_results = []
host = config[CONF_HOST]
self._url = 'http://{}/data/getConnectInfo.asp'.format(host)
self._loginurl = 'http://{}/goform/login'.format(host)
self._username = config.get(CONF_USERNAME)
self._password = config.get(CONF_PASSWORD)
self._userid = None
self.success_init = self._update_info()
_LOGGER.info("Scanner initialized")
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return [device.mac for device in self.last_results]
def get_device_name(self, mac):
"""Return the name of the device with the given MAC address."""
name = next((
device.name for device in self.last_results
if device.mac == mac), None)
return name
def _login(self):
"""Log in to the router. This is required for subsequent api calls."""
_LOGGER.info("Logging in to CODA...")
try:
data = [
('user', self._username),
('pws', self._password),
]
res = requests.post(self._loginurl, data=data, timeout=10)
except requests.exceptions.Timeout:
_LOGGER.error(
"Connection to the router timed out at URL %s", self._url)
return False
if res.status_code != 200:
_LOGGER.error(
"Connection failed with http code %s", res.status_code)
return False
try:
self._userid = res.cookies['userid']
return True
except KeyError:
_LOGGER.error("Failed to log in to router")
return False
def _update_info(self):
"""Get ARP from router."""
_LOGGER.info("Fetching...")
if self._userid is None:
if not self._login():
_LOGGER.error("Could not obtain a user ID from the router")
return False
last_results = []
# doing a request
try:
res = requests.get(self._url, timeout=10, cookies={
'userid': self._userid
})
except requests.exceptions.Timeout:
_LOGGER.error(
"Connection to the router timed out at URL %s", self._url)
return False
if res.status_code != 200:
_LOGGER.error(
"Connection failed with http code %s", res.status_code)
return False
try:
result = res.json()
except ValueError:
# If json decoder could not parse the response
_LOGGER.error("Failed to parse response from router")
return False
# parsing response
for info in result:
mac = info['macAddr']
name = info['hostName']
# No address = no item :)
if mac is None:
continue
last_results.append(Device(mac.upper(), name))
self.last_results = last_results
_LOGGER.info("Request successful")
return True
@@ -367,6 +367,29 @@ def async_handle_transition_message(hass, context, message):
message['event'])
@asyncio.coroutine
def async_handle_waypoint(hass, name_base, waypoint):
"""Handle a waypoint."""
name = waypoint['desc']
pretty_name = '{} - {}'.format(name_base, name)
lat = waypoint['lat']
lon = waypoint['lon']
rad = waypoint['rad']
# check zone exists
entity_id = zone_comp.ENTITY_ID_FORMAT.format(slugify(pretty_name))
# Check if state already exists
if hass.states.get(entity_id) is not None:
return
zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad,
zone_comp.ICON_IMPORT, False)
zone.entity_id = entity_id
yield from zone.async_update_ha_state()
@HANDLERS.register('waypoint')
@HANDLERS.register('waypoints')
@asyncio.coroutine
def async_handle_waypoints_message(hass, context, message):
@@ -380,30 +403,17 @@ def async_handle_waypoints_message(hass, context, message):
if user not in context.waypoint_whitelist:
return
wayps = message['waypoints']
if 'waypoints' in message:
wayps = message['waypoints']
else:
wayps = [message]
_LOGGER.info("Got %d waypoints from %s", len(wayps), message['topic'])
name_base = ' '.join(_parse_topic(message['topic']))
for wayp in wayps:
name = wayp['desc']
pretty_name = '{} - {}'.format(name_base, name)
lat = wayp['lat']
lon = wayp['lon']
rad = wayp['rad']
# check zone exists
entity_id = zone_comp.ENTITY_ID_FORMAT.format(slugify(pretty_name))
# Check if state already exists
if hass.states.get(entity_id) is not None:
continue
zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad,
zone_comp.ICON_IMPORT, False)
zone.entity_id = entity_id
yield from zone.async_update_ha_state()
yield from async_handle_waypoint(hass, name_base, wayp)
@HANDLERS.register('encrypted')
@@ -423,10 +433,22 @@ def async_handle_encrypted_message(hass, context, message):
@HANDLERS.register('lwt')
@HANDLERS.register('configuration')
@HANDLERS.register('beacon')
@HANDLERS.register('cmd')
@HANDLERS.register('steps')
@HANDLERS.register('card')
@asyncio.coroutine
def async_handle_lwt_message(hass, context, message):
"""Handle an lwt message."""
_LOGGER.debug('Not handling lwt message: %s', message)
def async_handle_not_impl_msg(hass, context, message):
"""Handle valid but not implemented message types."""
_LOGGER.debug('Not handling %s message: %s', message.get("_type"), message)
@asyncio.coroutine
def async_handle_unsupported_msg(hass, context, message):
"""Handle an unsupported or invalid message type."""
_LOGGER.warning('Received unsupported message type: %s.',
message.get('_type'))
@asyncio.coroutine
@@ -434,11 +456,6 @@ def async_handle_message(hass, context, message):
"""Handle an OwnTracks message."""
msgtype = message.get('_type')
handler = HANDLERS.get(msgtype)
if handler is None:
_LOGGER.warning(
'Received unsupported message type: %s.', msgtype)
return
handler = HANDLERS.get(msgtype, async_handle_unsupported_msg)
yield from handler(hass, context, message)
@@ -14,14 +14,14 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
REQUIREMENTS = ['pysnmp==4.4.2']
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pysnmp==4.4.1']
CONF_COMMUNITY = 'community'
CONF_AUTHKEY = 'authkey'
CONF_PRIVKEY = 'privkey'
CONF_BASEOID = 'baseoid'
CONF_COMMUNITY = 'community'
CONF_PRIVKEY = 'privkey'
DEFAULT_COMMUNITY = 'public'
@@ -6,13 +6,14 @@ https://home-assistant.io/components/device_tracker.swisscom/
"""
import logging
from aiohttp.hdrs import CONTENT_TYPE
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -77,7 +78,7 @@ class SwisscomDeviceScanner(DeviceScanner):
def get_swisscom_data(self):
"""Retrieve data from Swisscom and return parsed result."""
url = 'http://{}/ws'.format(self.host)
headers = {'Content-Type': 'application/x-sah-ws-4-call+json'}
headers = {CONTENT_TYPE: 'application/x-sah-ws-4-call+json'}
data = """
{"service":"Devices", "method":"get",
"parameters":{"expression":"lan and not self"}}"""
@@ -0,0 +1,124 @@
"""
Support for Tile® Bluetooth trackers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.tile/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import (
CONF_USERNAME, CONF_MONITORED_VARIABLES, CONF_PASSWORD)
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.util import slugify
from homeassistant.util.json import load_json, save_json
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pytile==1.0.0']
CLIENT_UUID_CONFIG_FILE = '.tile.conf'
DEFAULT_ICON = 'mdi:bluetooth'
DEVICE_TYPES = ['PHONE', 'TILE']
ATTR_ALTITUDE = 'altitude'
ATTR_CONNECTION_STATE = 'connection_state'
ATTR_IS_DEAD = 'is_dead'
ATTR_IS_LOST = 'is_lost'
ATTR_LAST_SEEN = 'last_seen'
ATTR_LAST_UPDATED = 'last_updated'
ATTR_RING_STATE = 'ring_state'
ATTR_VOIP_STATE = 'voip_state'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_MONITORED_VARIABLES):
vol.All(cv.ensure_list, [vol.In(DEVICE_TYPES)]),
})
def setup_scanner(hass, config: dict, see, discovery_info=None):
"""Validate the configuration and return a Tile scanner."""
TileDeviceScanner(hass, config, see)
return True
class TileDeviceScanner(DeviceScanner):
"""Define a device scanner for Tiles."""
def __init__(self, hass, config, see):
"""Initialize."""
from pytile import Client
_LOGGER.debug('Received configuration data: %s', config)
# Load the client UUID (if it exists):
config_data = load_json(hass.config.path(CLIENT_UUID_CONFIG_FILE))
if config_data:
_LOGGER.debug('Using existing client UUID')
self._client = Client(
config[CONF_USERNAME],
config[CONF_PASSWORD],
config_data['client_uuid'])
else:
_LOGGER.debug('Generating new client UUID')
self._client = Client(
config[CONF_USERNAME],
config[CONF_PASSWORD])
if not save_json(
hass.config.path(CLIENT_UUID_CONFIG_FILE),
{'client_uuid': self._client.client_uuid}):
_LOGGER.error("Failed to save configuration file")
_LOGGER.debug('Client UUID: %s', self._client.client_uuid)
_LOGGER.debug('User UUID: %s', self._client.user_uuid)
self._types = config.get(CONF_MONITORED_VARIABLES)
self.devices = {}
self.see = see
track_utc_time_change(
hass, self._update_info, second=range(0, 60, 30))
self._update_info()
def _update_info(self, now=None) -> None:
"""Update the device info."""
device_data = self._client.get_tiles(type_whitelist=self._types)
try:
self.devices = device_data['result']
except KeyError:
_LOGGER.warning('No Tiles found')
_LOGGER.debug(device_data)
return
for info in self.devices.values():
dev_id = 'tile_{0}'.format(slugify(info['name']))
lat = info['tileState']['latitude']
lon = info['tileState']['longitude']
attrs = {
ATTR_ALTITUDE: info['tileState']['altitude'],
ATTR_CONNECTION_STATE: info['tileState']['connection_state'],
ATTR_IS_DEAD: info['is_dead'],
ATTR_IS_LOST: info['tileState']['is_lost'],
ATTR_LAST_SEEN: info['tileState']['timestamp'],
ATTR_LAST_UPDATED: device_data['timestamp_ms'],
ATTR_RING_STATE: info['tileState']['ring_state'],
ATTR_VOIP_STATE: info['tileState']['voip_state'],
}
self.see(
dev_id=dev_id,
gps=(lat, lon),
attributes=attrs,
icon=DEFAULT_ICON
)
@@ -5,21 +5,27 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.tplink/
"""
import base64
from datetime import datetime
import hashlib
import logging
import re
from datetime import datetime
from aiohttp.hdrs import (
ACCEPT, COOKIE, PRAGMA, REFERER, CONNECTION, KEEP_ALIVE, USER_AGENT,
CONTENT_TYPE, CACHE_CONTROL, ACCEPT_ENCODING, ACCEPT_LANGUAGE)
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, HTTP_HEADER_X_REQUESTED_WITH)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
HTTP_HEADER_NO_CACHE = 'no-cache'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
@@ -78,7 +84,7 @@ class TplinkDeviceScanner(DeviceScanner):
referer = 'http://{}'.format(self.host)
page = requests.get(
url, auth=(self.username, self.password),
headers={'referer': referer}, timeout=4)
headers={REFERER: referer}, timeout=4)
result = self.parse_macs.findall(page.text)
@@ -123,7 +129,7 @@ class Tplink2DeviceScanner(TplinkDeviceScanner):
.format(b64_encoded_username_password)
response = requests.post(
url, headers={'referer': referer, 'cookie': cookie},
url, headers={REFERER: referer, COOKIE: cookie},
timeout=4)
try:
@@ -174,11 +180,11 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
.format(self.host)
referer = 'http://{}/webpages/login.html'.format(self.host)
# If possible implement rsa encryption of password here.
# If possible implement RSA encryption of password here.
response = requests.post(
url, params={'operation': 'login', 'username': self.username,
'password': self.password},
headers={'referer': referer}, timeout=4)
headers={REFERER: referer}, timeout=4)
try:
self.stok = response.json().get('data').get('stok')
@@ -207,11 +213,9 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
'form=statistics').format(self.host, self.stok)
referer = 'http://{}/webpages/index.html'.format(self.host)
response = requests.post(url,
params={'operation': 'load'},
headers={'referer': referer},
cookies={'sysauth': self.sysauth},
timeout=5)
response = requests.post(
url, params={'operation': 'load'}, headers={REFERER: referer},
cookies={'sysauth': self.sysauth}, timeout=5)
try:
json_response = response.json()
@@ -248,10 +252,9 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
'form=logout').format(self.host, self.stok)
referer = 'http://{}/webpages/index.html'.format(self.host)
requests.post(url,
params={'operation': 'write'},
headers={'referer': referer},
cookies={'sysauth': self.sysauth})
requests.post(
url, params={'operation': 'write'}, headers={REFERER: referer},
cookies={'sysauth': self.sysauth})
self.stok = ''
self.sysauth = ''
@@ -292,7 +295,7 @@ class Tplink4DeviceScanner(TplinkDeviceScanner):
# Create the authorization cookie.
cookie = 'Authorization=Basic {}'.format(self.credentials)
response = requests.get(url, headers={'cookie': cookie})
response = requests.get(url, headers={COOKIE: cookie})
try:
result = re.search(r'window.parent.location.href = '
@@ -326,8 +329,8 @@ class Tplink4DeviceScanner(TplinkDeviceScanner):
cookie = 'Authorization=Basic {}'.format(self.credentials)
page = requests.get(url, headers={
'cookie': cookie,
'referer': referer
COOKIE: cookie,
REFERER: referer,
})
mac_results.extend(self.parse_macs.findall(page.text))
@@ -361,31 +364,31 @@ class Tplink5DeviceScanner(TplinkDeviceScanner):
base_url = 'http://{}'.format(self.host)
header = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;"
" rv:53.0) Gecko/20100101 Firefox/53.0",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Language": "Accept-Language: en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate",
"Content-Type": "application/x-www-form-urlencoded; "
"charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
"Referer": "http://" + self.host + "/",
"Connection": "keep-alive",
"Pragma": "no-cache",
"Cache-Control": "no-cache"
USER_AGENT:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;"
" rv:53.0) Gecko/20100101 Firefox/53.0",
ACCEPT: "application/json, text/javascript, */*; q=0.01",
ACCEPT_LANGUAGE: "Accept-Language: en-US,en;q=0.5",
ACCEPT_ENCODING: "gzip, deflate",
CONTENT_TYPE: "application/x-www-form-urlencoded; charset=UTF-8",
HTTP_HEADER_X_REQUESTED_WITH: "XMLHttpRequest",
REFERER: "http://{}/".format(self.host),
CONNECTION: KEEP_ALIVE,
PRAGMA: HTTP_HEADER_NO_CACHE,
CACHE_CONTROL: HTTP_HEADER_NO_CACHE,
}
password_md5 = hashlib.md5(
self.password.encode('utf')).hexdigest().upper()
# create a session to handle cookie easier
# Create a session to handle cookie easier
session = requests.session()
session.get(base_url, headers=header)
login_data = {"username": self.username, "password": password_md5}
session.post(base_url, login_data, headers=header)
# a timestamp is required to be sent as get parameter
# A timestamp is required to be sent as get parameter
timestamp = int(datetime.now().timestamp() * 1e3)
client_list_url = '{}/data/monitor.client.client.json'.format(
@@ -393,18 +396,17 @@ class Tplink5DeviceScanner(TplinkDeviceScanner):
get_params = {
'operation': 'load',
'_': timestamp
'_': timestamp,
}
response = session.get(client_list_url,
headers=header,
params=get_params)
response = session.get(
client_list_url, headers=header, params=get_params)
session.close()
try:
list_of_devices = response.json()
except ValueError:
_LOGGER.error("AP didn't respond with JSON. "
"Check if credentials are correct.")
"Check if credentials are correct")
return False
if list_of_devices:
@@ -8,28 +8,28 @@ import asyncio
import logging
import aiohttp
from aiohttp.hdrs import REFERER, USER_AGENT
import async_timeout
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
from homeassistant.const import CONF_HOST, HTTP_HEADER_X_REQUESTED_WITH
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['defusedxml==0.5.0']
_LOGGER = logging.getLogger(__name__)
CMD_DEVICES = 123
DEFAULT_IP = '192.168.0.1'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string,
})
CMD_DEVICES = 123
@asyncio.coroutine
def async_get_scanner(hass, config):
@@ -52,11 +52,11 @@ class UPCDeviceScanner(DeviceScanner):
self.token = None
self.headers = {
'X-Requested-With': 'XMLHttpRequest',
'Referer': "http://{}/index.html".format(self.host),
'User-Agent': ("Mozilla/5.0 (Windows NT 10.0; WOW64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/47.0.2526.106 Safari/537.36")
HTTP_HEADER_X_REQUESTED_WITH: 'XMLHttpRequest',
REFERER: "http://{}/index.html".format(self.host),
USER_AGENT: ("Mozilla/5.0 (Windows NT 10.0; WOW64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/47.0.2526.106 Safari/537.36")
}
self.websession = async_get_clientsession(hass)
@@ -95,8 +95,7 @@ class UPCDeviceScanner(DeviceScanner):
with async_timeout.timeout(10, loop=self.hass.loop):
response = yield from self.websession.get(
"http://{}/common_page/login.html".format(self.host),
headers=self.headers
)
headers=self.headers)
yield from response.text()
@@ -118,17 +117,15 @@ class UPCDeviceScanner(DeviceScanner):
response = yield from self.websession.post(
"http://{}/xml/getter.xml".format(self.host),
data="token={}&fun={}".format(self.token, function),
headers=self.headers,
allow_redirects=False
)
headers=self.headers, allow_redirects=False)
# error?
# Error?
if response.status != 200:
_LOGGER.warning("Receive http code %d", response.status)
self.token = None
return
# load data, store token for next request
# Load data, store token for next request
self.token = response.cookies['sessionToken'].value
return (yield from response.text())
+4 -2
View File
@@ -9,7 +9,7 @@ import logging
import voluptuous as vol
from homeassistant.const import PROJECT_NAME, HTTP_BAD_REQUEST
from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import intent, template
from homeassistant.components.http import HomeAssistantView
@@ -26,6 +26,8 @@ DOMAIN = 'dialogflow'
INTENTS_API_ENDPOINT = '/api/dialogflow'
SOURCE = "Home Assistant Dialogflow"
CONFIG_SCHEMA = vol.Schema({
DOMAIN: {}
}, extra=vol.ALLOW_EXTRA)
@@ -128,5 +130,5 @@ class DialogflowResponse(object):
return {
'speech': self.speech,
'displayText': self.speech,
'source': PROJECT_NAME,
'source': SOURCE,
}
+18 -8
View File
@@ -20,6 +20,7 @@ _LOGGER = logging.getLogger(__name__)
ATTR_FILENAME = 'filename'
ATTR_SUBDIR = 'subdir'
ATTR_URL = 'url'
ATTR_OVERWRITE = 'overwrite'
CONF_DOWNLOAD_DIR = 'download_dir'
@@ -31,6 +32,7 @@ SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({
vol.Required(ATTR_URL): cv.url,
vol.Optional(ATTR_SUBDIR): cv.string,
vol.Optional(ATTR_FILENAME): cv.string,
vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean,
})
CONFIG_SCHEMA = vol.Schema({
@@ -66,6 +68,8 @@ def setup(hass, config):
filename = service.data.get(ATTR_FILENAME)
overwrite = service.data.get(ATTR_OVERWRITE)
if subdir:
subdir = sanitize_filename(subdir)
@@ -73,8 +77,13 @@ def setup(hass, config):
req = requests.get(url, stream=True, timeout=10)
if req.status_code == 200:
if req.status_code != 200:
_LOGGER.warning(
"downloading '%s' failed, stauts_code=%d",
url,
req.status_code)
else:
if filename is None and \
'content-disposition' in req.headers:
match = re.findall(r"filename=(\S+)",
@@ -109,20 +118,21 @@ def setup(hass, config):
# If file exist append a number.
# We test filename, filename_2..
tries = 1
final_path = path + ext
while os.path.isfile(final_path):
tries += 1
if not overwrite:
tries = 1
final_path = path + ext
while os.path.isfile(final_path):
tries += 1
final_path = "{}_{}.{}".format(path, tries, ext)
final_path = "{}_{}.{}".format(path, tries, ext)
_LOGGER.info("%s -> %s", url, final_path)
_LOGGER.debug("%s -> %s", url, final_path)
with open(final_path, 'wb') as fil:
for chunk in req.iter_content(1024):
fil.write(chunk)
_LOGGER.info("Downloading of %s done", url)
_LOGGER.debug("Downloading of %s done", url)
except requests.exceptions.ConnectionError:
_LOGGER.exception("ConnectionError occurred for %s", url)
+6 -5
View File
@@ -72,6 +72,7 @@ class EnOceanDongle:
"""
from enocean.protocol.packet import RadioPacket
if isinstance(temp, RadioPacket):
_LOGGER.debug("Received radio packet: %s", temp)
rxtype = None
value = None
if temp.data[6] == 0x30:
@@ -94,20 +95,20 @@ class EnOceanDongle:
value = temp.data[2]
for device in self.__devices:
if rxtype == "wallswitch" and device.stype == "listener":
if temp.sender == self._combine_hex(device.dev_id):
if temp.sender_int == self._combine_hex(device.dev_id):
device.value_changed(value, temp.data[1])
if rxtype == "power" and device.stype == "powersensor":
if temp.sender == self._combine_hex(device.dev_id):
if temp.sender_int == self._combine_hex(device.dev_id):
device.value_changed(value)
if rxtype == "power" and device.stype == "switch":
if temp.sender == self._combine_hex(device.dev_id):
if temp.sender_int == self._combine_hex(device.dev_id):
if value > 10:
device.value_changed(1)
if rxtype == "switch_status" and device.stype == "switch":
if temp.sender == self._combine_hex(device.dev_id):
if temp.sender_int == self._combine_hex(device.dev_id):
device.value_changed(value)
if rxtype == "dimmerstatus" and device.stype == "dimmer":
if temp.sender == self._combine_hex(device.dev_id):
if temp.sender_int == self._combine_hex(device.dev_id):
device.value_changed(value)
+1 -1
View File
@@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
REQUIREMENTS = ['python-miio==0.3.0']
REQUIREMENTS = ['python-miio==0.3.1']
ATTR_TEMPERATURE = 'temperature'
ATTR_HUMIDITY = 'humidity'
+117 -72
View File
@@ -9,9 +9,11 @@ import hashlib
import json
import logging
import os
from urllib.parse import urlparse
from aiohttp import web
import voluptuous as vol
import jinja2
import homeassistant.helpers.config_validation as cv
from homeassistant.components.http import HomeAssistantView
@@ -21,21 +23,19 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback
from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20171105.0']
REQUIREMENTS = ['home-assistant-frontend==20171118.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api']
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
POLYMER_PATH = os.path.join(os.path.dirname(__file__),
'home-assistant-polymer/')
FINAL_PATH = os.path.join(POLYMER_PATH, 'final')
CONF_THEMES = 'themes'
CONF_EXTRA_HTML_URL = 'extra_html_url'
CONF_FRONTEND_REPO = 'development_repo'
CONF_JS_VERSION = 'javascript_version'
JS_DEFAULT_OPTION = 'es5'
JS_OPTIONS = ['es5', 'latest', 'auto']
DEFAULT_THEME_COLOR = '#03A9F4'
@@ -61,6 +61,7 @@ for size in (192, 384, 512, 1024):
DATA_FINALIZE_PANEL = 'frontend_finalize_panel'
DATA_PANELS = 'frontend_panels'
DATA_JS_VERSION = 'frontend_js_version'
DATA_EXTRA_HTML_URL = 'frontend_extra_html_url'
DATA_THEMES = 'frontend_themes'
DATA_DEFAULT_THEME = 'frontend_default_theme'
@@ -68,8 +69,6 @@ DEFAULT_THEME = 'default'
PRIMARY_COLOR = 'primary-color'
# To keep track we don't register a component twice (gives a warning)
# _REGISTERED_COMPONENTS = set()
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({
@@ -80,6 +79,8 @@ CONFIG_SCHEMA = vol.Schema({
}),
vol.Optional(CONF_EXTRA_HTML_URL):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_JS_VERSION, default=JS_DEFAULT_OPTION):
vol.In(JS_OPTIONS)
}),
}, extra=vol.ALLOW_EXTRA)
@@ -102,8 +103,9 @@ class AbstractPanel:
# Title to show in the sidebar (optional)
sidebar_title = None
# Url to the webcomponent
webcomponent_url = None
# Url to the webcomponent (depending on JS version)
webcomponent_url_es5 = None
webcomponent_url_latest = None
# Url to show the panel in the frontend
frontend_url_path = None
@@ -135,16 +137,20 @@ class AbstractPanel:
'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path),
index_view.get)
def as_dict(self):
def to_response(self, hass, request):
"""Panel as dictionary."""
return {
result = {
'component_name': self.component_name,
'icon': self.sidebar_icon,
'title': self.sidebar_title,
'url': self.webcomponent_url,
'url_path': self.frontend_url_path,
'config': self.config,
}
if _is_latest(hass.data[DATA_JS_VERSION], request):
result['url'] = self.webcomponent_url_latest
else:
result['url'] = self.webcomponent_url_es5
return result
class BuiltInPanel(AbstractPanel):
@@ -166,19 +172,21 @@ class BuiltInPanel(AbstractPanel):
If frontend_repository_path is set, will be prepended to path of
built-in components.
"""
panel_path = 'panels/ha-panel-{}.html'.format(self.component_name)
if frontend_repository_path is None:
import hass_frontend
import hass_frontend_es5
self.webcomponent_url = \
'/static/panels/ha-panel-{}-{}.html'.format(
self.webcomponent_url_latest = \
'/frontend_latest/panels/ha-panel-{}-{}.html'.format(
self.component_name,
hass_frontend.FINGERPRINTS[panel_path])
hass_frontend.FINGERPRINTS[self.component_name])
self.webcomponent_url_es5 = \
'/frontend_es5/panels/ha-panel-{}-{}.html'.format(
self.component_name,
hass_frontend_es5.FINGERPRINTS[self.component_name])
else:
# Dev mode
self.webcomponent_url = \
self.webcomponent_url_es5 = self.webcomponent_url_latest = \
'/home-assistant-polymer/panels/{}/ha-panel-{}.html'.format(
self.component_name, self.component_name)
@@ -208,18 +216,20 @@ class ExternalPanel(AbstractPanel):
"""
try:
if self.md5 is None:
yield from hass.async_add_job(_fingerprint, self.path)
self.md5 = yield from hass.async_add_job(
_fingerprint, self.path)
except OSError:
_LOGGER.error('Cannot find or access %s at %s',
self.component_name, self.path)
hass.data[DATA_PANELS].pop(self.frontend_url_path)
return
self.webcomponent_url = \
self.webcomponent_url_es5 = self.webcomponent_url_latest = \
URL_PANEL_COMPONENT_FP.format(self.component_name, self.md5)
if self.component_name not in self.REGISTERED_COMPONENTS:
hass.http.register_static_path(
self.webcomponent_url, self.path,
self.webcomponent_url_latest, self.path,
# if path is None, we're in prod mode, so cache static assets
frontend_repository_path is None)
self.REGISTERED_COMPONENTS.add(self.component_name)
@@ -281,31 +291,50 @@ def async_setup(hass, config):
repo_path = conf.get(CONF_FRONTEND_REPO)
is_dev = repo_path is not None
hass.data[DATA_JS_VERSION] = js_version = conf.get(CONF_JS_VERSION)
if is_dev:
hass.http.register_static_path(
"/home-assistant-polymer", repo_path, False)
hass.http.register_static_path(
"/static/translations",
os.path.join(repo_path, "build/translations"), False)
sw_path = os.path.join(repo_path, "build/service_worker.js")
os.path.join(repo_path, "build-translations"), False)
sw_path_es5 = os.path.join(repo_path, "build-es5/service_worker.js")
sw_path_latest = os.path.join(repo_path, "build/service_worker.js")
static_path = os.path.join(repo_path, 'hass_frontend')
frontend_es5_path = os.path.join(repo_path, 'build-es5')
frontend_latest_path = os.path.join(repo_path, 'build')
else:
import hass_frontend
frontend_path = hass_frontend.where()
sw_path = os.path.join(frontend_path, "service_worker.js")
static_path = frontend_path
import hass_frontend_es5
sw_path_es5 = os.path.join(hass_frontend_es5.where(),
"service_worker.js")
sw_path_latest = os.path.join(hass_frontend.where(),
"service_worker.js")
# /static points to dir with files that are JS-type agnostic.
# ES5 files are served from /frontend_es5.
# ES6 files are served from /frontend_latest.
static_path = hass_frontend.where()
frontend_es5_path = hass_frontend_es5.where()
frontend_latest_path = static_path
hass.http.register_static_path("/service_worker.js", sw_path, False)
hass.http.register_static_path(
"/service_worker_es5.js", sw_path_es5, False)
hass.http.register_static_path(
"/service_worker.js", sw_path_latest, False)
hass.http.register_static_path(
"/robots.txt", os.path.join(static_path, "robots.txt"), not is_dev)
hass.http.register_static_path("/static", static_path, not is_dev)
hass.http.register_static_path(
"/frontend_latest", frontend_latest_path, not is_dev)
hass.http.register_static_path(
"/frontend_es5", frontend_es5_path, not is_dev)
local = hass.config.path('www')
if os.path.isdir(local):
hass.http.register_static_path("/local", local, not is_dev)
index_view = IndexView(is_dev)
index_view = IndexView(repo_path, js_version)
hass.http.register_view(index_view)
@asyncio.coroutine
@@ -405,40 +434,40 @@ class IndexView(HomeAssistantView):
requires_auth = False
extra_urls = ['/states', '/states/{extra}']
def __init__(self, use_repo):
def __init__(self, repo_path, js_option):
"""Initialize the frontend view."""
from jinja2 import FileSystemLoader, Environment
self.repo_path = repo_path
self.js_option = js_option
self._template_cache = {}
self.use_repo = use_repo
self.templates = Environment(
autoescape=True,
loader=FileSystemLoader(
os.path.join(os.path.dirname(__file__), 'templates/')
)
)
def get_template(self, latest):
"""Get template."""
if self.repo_path is not None:
root = self.repo_path
elif latest:
import hass_frontend
root = hass_frontend.where()
else:
import hass_frontend_es5
root = hass_frontend_es5.where()
tpl = self._template_cache.get(root)
if tpl is None:
with open(os.path.join(root, 'index.html')) as file:
tpl = jinja2.Template(file.read())
# Cache template if not running from repository
if self.repo_path is None:
self._template_cache[root] = tpl
return tpl
@asyncio.coroutine
def get(self, request, extra=None):
"""Serve the index view."""
hass = request.app['hass']
if self.use_repo:
core_url = '/home-assistant-polymer/build/core.js'
compatibility_url = \
'/home-assistant-polymer/build/compatibility.js'
ui_url = '/home-assistant-polymer/src/home-assistant.html'
icons_fp = ''
icons_url = '/static/mdi.html'
else:
import hass_frontend
core_url = '/static/core-{}.js'.format(
hass_frontend.FINGERPRINTS['core.js'])
compatibility_url = '/static/compatibility-{}.js'.format(
hass_frontend.FINGERPRINTS['compatibility.js'])
ui_url = '/static/frontend-{}.html'.format(
hass_frontend.FINGERPRINTS['frontend.html'])
icons_fp = '-{}'.format(hass_frontend.FINGERPRINTS['mdi.html'])
icons_url = '/static/mdi{}.html'.format(icons_fp)
latest = _is_latest(self.js_option, request)
if request.path == '/':
panel = 'states'
@@ -447,28 +476,27 @@ class IndexView(HomeAssistantView):
if panel == 'states':
panel_url = ''
elif latest:
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_latest
else:
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_es5
no_auth = 'true'
if hass.config.api.api_password and not is_trusted_ip(request):
# do not try to auto connect on load
no_auth = 'false'
template = yield from hass.async_add_job(
self.templates.get_template, 'index.html')
template = yield from hass.async_add_job(self.get_template, latest)
# pylint is wrong
# pylint: disable=no-member
# This is a jinja2 template, not a HA template so we call 'render'.
resp = template.render(
core_url=core_url, ui_url=ui_url,
compatibility_url=compatibility_url, no_auth=no_auth,
icons_url=icons_url, icons=icons_fp,
panel_url=panel_url, panels=hass.data[DATA_PANELS],
dev_mode=self.use_repo,
no_auth=no_auth,
panel_url=panel_url,
panels=hass.data[DATA_PANELS],
dev_mode=self.repo_path is not None,
theme_color=MANIFEST_JSON['theme_color'],
extra_urls=hass.data[DATA_EXTRA_HTML_URL])
extra_urls=hass.data[DATA_EXTRA_HTML_URL],
latest=latest,
)
return web.Response(text=resp, content_type='text/html')
@@ -483,8 +511,8 @@ class ManifestJSONView(HomeAssistantView):
@asyncio.coroutine
def get(self, request): # pylint: disable=no-self-use
"""Return the manifest.json."""
msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8')
return web.Response(body=msg, content_type="application/manifest+json")
msg = json.dumps(MANIFEST_JSON, sort_keys=True)
return web.Response(text=msg, content_type="application/manifest+json")
class ThemesView(HomeAssistantView):
@@ -509,3 +537,20 @@ def _fingerprint(path):
"""Fingerprint a file."""
with open(path) as fil:
return hashlib.md5(fil.read().encode('utf-8')).hexdigest()
def _is_latest(js_option, request):
"""
Return whether we should serve latest untranspiled code.
Set according to user's preference and URL override.
"""
if request is None:
return js_option == 'latest'
latest_in_query = 'latest' in request.query or (
request.headers.get('Referer') and
'latest' in urlparse(request.headers['Referer']).query)
es5_in_query = 'es5' in request.query or (
request.headers.get('Referer') and
'es5' in urlparse(request.headers['Referer']).query)
return latest_in_query or (not es5_in_query and js_option == 'latest')
@@ -1,118 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Home Assistant</title>
<link rel='manifest' href='/manifest.json'>
<link rel='icon' href='/static/icons/favicon.ico'>
<link rel='apple-touch-icon' sizes='180x180'
href='/static/icons/favicon-apple-180x180.png'>
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#3fbbf4">
{% if not dev_mode %}
<link rel='preload' href='{{ core_url }}' as='script'/>
{% for panel in panels.values() -%}
<link rel='prefetch' href='{{ panel.webcomponent_url }}'>
{% endfor -%}
{% endif %}
<meta name='apple-mobile-web-app-capable' content='yes'>
<meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/>
<meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/>
<meta name="msapplication-wide310x150logo" content="/static/icons/tile-win-310x150.png"/>
<meta name="msapplication-square310x310logo" content="/static/icons/tile-win-310x310.png"/>
<meta name="msapplication-TileColor" content="#3fbbf4ff"/>
<meta name='mobile-web-app-capable' content='yes'>
<meta name='viewport' content='width=device-width, user-scalable=no'>
<meta name='theme-color' content='{{ theme_color }}'>
<style>
body {
font-family: 'Roboto', 'Noto', sans-serif;
font-weight: 400;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
margin: 0;
padding: 0;
}
#ha-init-skeleton::before {
display: block;
content: "";
height: 48px;
background-color: {{ theme_color }};
}
#ha-init-skeleton .message {
transition: font-size 2s;
font-size: 0;
padding: 24px;
}
#ha-init-skeleton.error .message {
font-size: 16px;
}
#ha-init-skeleton a {
color: {{ theme_color }};
text-decoration: none;
font-weight: bold;
}
</style>
<script>
function initError() {
document.getElementById('ha-init-skeleton').classList.add('error');
};
window.noAuth = {{ no_auth }};
window.Polymer = {
lazyRegister: true,
useNativeCSSProperties: true,
dom: 'shadow',
suppressTemplateNotifications: true,
suppressBindingNotifications: true,
};
</script>
</head>
<body>
<div id='ha-init-skeleton'>
<div class='message'>
Home Assistant had trouble<br>connecting to the server.<br><br>
<a href='/'>TRY AGAIN</a>
</div>
</div>
<home-assistant icons='{{ icons }}'></home-assistant>
{# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #}
<script>
var compatibilityRequired = (
typeof Object.assign != 'function');
if (compatibilityRequired) {
var e = document.createElement('script');
e.onerror = initError;
e.src = '{{ compatibility_url }}';
document.head.appendChild(e);
}
</script>
<script src='{{ core_url }}'></script>
{% if not dev_mode %}
<script src='/static/custom-elements-es5-adapter.js'></script>
{% endif %}
<script>
var webComponentsSupported = (
'customElements' in window &&
'import' in document.createElement('link') &&
'content' in document.createElement('template'));
if (!webComponentsSupported) {
var e = document.createElement('script');
e.onerror = initError;
e.src = '/static/webcomponents-lite.js';
document.head.appendChild(e);
}
</script>
<link rel='import' href='{{ ui_url }}' onerror='initError()'>
{% if panel_url -%}
<link rel='import' href='{{ panel_url }}' onerror='initError()' async>
{% endif -%}
<link rel='import' href='{{ icons_url }}' async>
{% for extra_url in extra_urls -%}
<link rel='import' href='{{ extra_url }}' async>
{% endfor -%}
</body>
</html>
+2 -2
View File
@@ -12,7 +12,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-gc100==1.0.1a']
REQUIREMENTS = ['python-gc100==1.0.3a']
_LOGGER = logging.getLogger(__name__)
@@ -42,7 +42,7 @@ def setup(hass, base_config):
gc_device = gc100.GC100SocketClient(host, port)
def cleanup_gc100():
def cleanup_gc100(event):
"""Stuff to do before stopping."""
gc_device.quit()
@@ -4,9 +4,13 @@ Support for Actions on Google Assistant Smart Home Control.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/google_assistant/
"""
import os
import asyncio
import logging
import aiohttp
import async_timeout
import voluptuous as vol
# Typing imports
@@ -15,11 +19,16 @@ import voluptuous as vol
from homeassistant.core import HomeAssistant # NOQA
from typing import Dict, Any # NOQA
from homeassistant import config as conf_util
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.loader import bind_hass
from .const import (
DOMAIN, CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN,
CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS
CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS,
CONF_AGENT_USER_ID, CONF_API_KEY,
SERVICE_REQUEST_SYNC, REQUEST_SYNC_BASE_URL
)
from .auth import GoogleAssistantAuthView
from .http import GoogleAssistantView
@@ -28,6 +37,8 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http']
DEFAULT_AGENT_USER_ID = 'home-assistant'
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: {
@@ -36,17 +47,57 @@ CONFIG_SCHEMA = vol.Schema(
vol.Required(CONF_ACCESS_TOKEN): cv.string,
vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean,
vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list,
vol.Optional(CONF_AGENT_USER_ID,
default=DEFAULT_AGENT_USER_ID): cv.string,
vol.Optional(CONF_API_KEY): cv.string
}
},
extra=vol.ALLOW_EXTRA)
@bind_hass
def request_sync(hass):
"""Request sync."""
hass.services.call(DOMAIN, SERVICE_REQUEST_SYNC)
@asyncio.coroutine
def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
"""Activate Google Actions component."""
config = yaml_config.get(DOMAIN, {})
agent_user_id = config.get(CONF_AGENT_USER_ID)
api_key = config.get(CONF_API_KEY)
if api_key is not None:
descriptions = yield from hass.async_add_job(
conf_util.load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml')
)
hass.http.register_view(GoogleAssistantAuthView(hass, config))
hass.http.register_view(GoogleAssistantView(hass, config))
@asyncio.coroutine
def request_sync_service_handler(call):
"""Handle request sync service calls."""
websession = async_get_clientsession(hass)
try:
with async_timeout.timeout(5, loop=hass.loop):
res = yield from websession.post(
REQUEST_SYNC_BASE_URL,
params={'key': api_key},
json={'agent_user_id': agent_user_id})
_LOGGER.info("Submitted request_sync request to Google")
res.raise_for_status()
except aiohttp.ClientResponseError:
body = yield from res.read()
_LOGGER.error(
'request_sync request failed: %d %s', res.status, body)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Could not contact Google for request_sync")
# Register service only if api key is provided
if api_key is not None:
hass.services.async_register(
DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler,
descriptions.get(SERVICE_REQUEST_SYNC))
return True
@@ -13,6 +13,8 @@ CONF_PROJECT_ID = 'project_id'
CONF_ACCESS_TOKEN = 'access_token'
CONF_CLIENT_ID = 'client_id'
CONF_ALIASES = 'aliases'
CONF_AGENT_USER_ID = 'agent_user_id'
CONF_API_KEY = 'api_key'
DEFAULT_EXPOSE_BY_DEFAULT = True
DEFAULT_EXPOSED_DOMAINS = [
@@ -44,3 +46,7 @@ TYPE_LIGHT = PREFIX_TYPES + 'LIGHT'
TYPE_SWITCH = PREFIX_TYPES + 'SWITCH'
TYPE_SCENE = PREFIX_TYPES + 'SCENE'
TYPE_THERMOSTAT = PREFIX_TYPES + 'THERMOSTAT'
SERVICE_REQUEST_SYNC = 'request_sync'
HOMEGRAPH_URL = 'https://homegraph.googleapis.com/'
REQUEST_SYNC_BASE_URL = HOMEGRAPH_URL + 'v1/devices:requestSync'
@@ -7,17 +7,18 @@ https://home-assistant.io/components/google_assistant/
import asyncio
import logging
from typing import Any, Dict # NOQA
from aiohttp.hdrs import AUTHORIZATION
from aiohttp.web import Request, Response # NOQA
# Typing imports
# pylint: disable=using-constant-test,unused-import,ungrouped-imports
# if False:
from homeassistant.core import HomeAssistant # NOQA
from aiohttp.web import Request, Response # NOQA
from typing import Dict, Tuple, Any # NOQA
from homeassistant.helpers.entity import Entity # NOQA
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED)
from homeassistant.const import HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED
from homeassistant.core import HomeAssistant # NOQA
from homeassistant.helpers.entity import Entity # NOQA
from .const import (
GOOGLE_ASSISTANT_API_ENDPOINT,
@@ -26,7 +27,9 @@ from .const import (
DEFAULT_EXPOSED_DOMAINS,
CONF_EXPOSE_BY_DEFAULT,
CONF_EXPOSED_DOMAINS,
ATTR_GOOGLE_ASSISTANT)
ATTR_GOOGLE_ASSISTANT,
CONF_AGENT_USER_ID
)
from .smart_home import entity_to_device, query_device, determine_service
_LOGGER = logging.getLogger(__name__)
@@ -48,6 +51,7 @@ class GoogleAssistantView(HomeAssistantView):
DEFAULT_EXPOSE_BY_DEFAULT)
self.exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS,
DEFAULT_EXPOSED_DOMAINS)
self.agent_user_id = cfg.get(CONF_AGENT_USER_ID)
def is_entity_exposed(self, entity) -> bool:
"""Determine if an entity should be exposed to Google Assistant."""
@@ -77,7 +81,7 @@ class GoogleAssistantView(HomeAssistantView):
if not self.is_entity_exposed(entity):
continue
device = entity_to_device(entity)
device = entity_to_device(entity, hass.config.units)
if device is None:
_LOGGER.warning("No mapping for %s domain", entity.domain)
continue
@@ -85,7 +89,9 @@ class GoogleAssistantView(HomeAssistantView):
devices.append(device)
return self.json(
make_actions_response(request_id, {'devices': devices}))
_make_actions_response(request_id,
{'agentUserId': self.agent_user_id,
'devices': devices}))
@asyncio.coroutine
def handle_query(self,
@@ -106,10 +112,10 @@ class GoogleAssistantView(HomeAssistantView):
# If we can't find a state, the device is offline
devices[devid] = {'online': False}
devices[devid] = query_device(state)
devices[devid] = query_device(state, hass.config.units)
return self.json(
make_actions_response(request_id, {'devices': devices}))
_make_actions_response(request_id, {'devices': devices}))
@asyncio.coroutine
def handle_execute(self,
@@ -122,9 +128,11 @@ class GoogleAssistantView(HomeAssistantView):
ent_ids = [ent.get('id') for ent in command.get('devices', [])]
execution = command.get('execution')[0]
for eid in ent_ids:
success = False
domain = eid.split('.')[0]
(service, service_data) = determine_service(
eid, execution.get('command'), execution.get('params'))
eid, execution.get('command'), execution.get('params'),
hass.config.units)
success = yield from hass.services.async_call(
domain, service, service_data, blocking=True)
result = {"ids": [eid], "states": {}}
@@ -135,12 +143,12 @@ class GoogleAssistantView(HomeAssistantView):
commands.append(result)
return self.json(
make_actions_response(request_id, {'commands': commands}))
_make_actions_response(request_id, {'commands': commands}))
@asyncio.coroutine
def post(self, request: Request) -> Response:
"""Handle Google Assistant requests."""
auth = request.headers.get('Authorization', None)
auth = request.headers.get(AUTHORIZATION, None)
if 'Bearer {}'.format(self.access_token) != auth:
return self.json_message(
"missing authorization", status_code=HTTP_UNAUTHORIZED)
@@ -175,6 +183,5 @@ class GoogleAssistantView(HomeAssistantView):
"invalid intent", status_code=HTTP_BAD_REQUEST)
def make_actions_response(request_id: str, payload: dict) -> dict:
"""Helper to simplify format for response."""
def _make_actions_response(request_id: str, payload: dict) -> dict:
return {'requestId': request_id, 'payload': payload}
@@ -0,0 +1,2 @@
request_sync:
description: Send a request_sync command to Google.
@@ -5,21 +5,26 @@ import logging
# pylint: disable=using-constant-test,unused-import,ungrouped-imports
# if False:
from aiohttp.web import Request, Response # NOQA
from typing import Dict, Tuple, Any # NOQA
from typing import Dict, Tuple, Any, Optional # NOQA
from homeassistant.helpers.entity import Entity # NOQA
from homeassistant.core import HomeAssistant # NOQA
from homeassistant.util import color
from homeassistant.util.unit_system import UnitSystem # NOQA
from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID,
CONF_FRIENDLY_NAME, STATE_OFF,
SERVICE_TURN_OFF, SERVICE_TURN_ON
SERVICE_TURN_OFF, SERVICE_TURN_ON,
TEMP_FAHRENHEIT, TEMP_CELSIUS,
)
from homeassistant.components import (
switch, light, cover, media_player, group, fan, scene, script, climate
)
from homeassistant.util.unit_system import METRIC_SYSTEM
from .const import (
ATTR_GOOGLE_ASSISTANT_NAME, ATTR_GOOGLE_ASSISTANT_TYPE,
ATTR_GOOGLE_ASSISTANT_NAME, COMMAND_COLOR,
ATTR_GOOGLE_ASSISTANT_TYPE,
COMMAND_BRIGHTNESS, COMMAND_ONOFF, COMMAND_ACTIVATESCENE,
COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT,
COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE, COMMAND_THERMOSTAT_SET_MODE,
@@ -65,7 +70,7 @@ def make_actions_response(request_id: str, payload: dict) -> dict:
return {'requestId': request_id, 'payload': payload}
def entity_to_device(entity: Entity):
def entity_to_device(entity: Entity, units: UnitSystem):
"""Convert a hass entity into an google actions device."""
class_data = MAPPING_COMPONENT.get(
entity.attributes.get(ATTR_GOOGLE_ASSISTANT_TYPE) or entity.domain)
@@ -75,6 +80,7 @@ def entity_to_device(entity: Entity):
device = {
'id': entity.entity_id,
'name': {},
'attributes': {},
'traits': [],
'willReportState': False,
}
@@ -99,20 +105,62 @@ def entity_to_device(entity: Entity):
for feature, trait in class_data[2].items():
if feature & supported > 0:
device['traits'].append(trait)
# Actions require this attributes for a device
# supporting temperature
# For IKEA trådfri, these attributes only seem to
# be set only if the device is on?
if trait == TRAIT_COLOR_TEMP:
if entity.attributes.get(
light.ATTR_MAX_MIREDS) is not None:
device['attributes']['temperatureMinK'] = \
int(round(color.color_temperature_mired_to_kelvin(
entity.attributes.get(light.ATTR_MAX_MIREDS))))
if entity.attributes.get(
light.ATTR_MIN_MIREDS) is not None:
device['attributes']['temperatureMaxK'] = \
int(round(color.color_temperature_mired_to_kelvin(
entity.attributes.get(light.ATTR_MIN_MIREDS))))
if entity.domain == climate.DOMAIN:
modes = ','.join(
m for m in entity.attributes.get(climate.ATTR_OPERATION_LIST, [])
if m in CLIMATE_SUPPORTED_MODES)
device['attributes'] = {
'availableThermostatModes': modes,
'thermostatTemperatureUnit': 'C',
'thermostatTemperatureUnit':
'F' if units.temperature_unit == TEMP_FAHRENHEIT else 'C',
}
return device
def query_device(entity: Entity) -> dict:
def query_device(entity: Entity, units: UnitSystem) -> dict:
"""Take an entity and return a properly formatted device object."""
def celsius(deg: Optional[float]) -> Optional[float]:
"""Convert a float to Celsius and rounds to one decimal place."""
if deg is None:
return None
return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1)
if entity.domain == climate.DOMAIN:
mode = entity.attributes.get(climate.ATTR_OPERATION_MODE)
if mode not in CLIMATE_SUPPORTED_MODES:
mode = 'on'
response = {
'thermostatMode': mode,
'thermostatTemperatureSetpoint':
celsius(entity.attributes.get(climate.ATTR_TEMPERATURE)),
'thermostatTemperatureAmbient':
celsius(entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE)),
'thermostatTemperatureSetpointHigh':
celsius(entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)),
'thermostatTemperatureSetpointLow':
celsius(entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)),
'thermostatHumidityAmbient':
entity.attributes.get(climate.ATTR_CURRENT_HUMIDITY),
}
return {k: v for k, v in response.items() if v is not None}
final_state = entity.state != STATE_OFF
final_brightness = entity.attributes.get(light.ATTR_BRIGHTNESS, 255
if final_state else 0)
@@ -128,18 +176,42 @@ def query_device(entity: Entity) -> dict:
final_brightness = 100 * (final_brightness / 255)
return {
query_response = {
"on": final_state,
"online": True,
"brightness": int(final_brightness)
}
supported_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported_features & \
(light.SUPPORT_COLOR_TEMP | light.SUPPORT_RGB_COLOR):
query_response["color"] = {}
if entity.attributes.get(light.ATTR_COLOR_TEMP) is not None:
query_response["color"]["temperature"] = \
int(round(color.color_temperature_mired_to_kelvin(
entity.attributes.get(light.ATTR_COLOR_TEMP))))
if entity.attributes.get(light.ATTR_COLOR_NAME) is not None:
query_response["color"]["name"] = \
entity.attributes.get(light.ATTR_COLOR_NAME)
if entity.attributes.get(light.ATTR_RGB_COLOR) is not None:
color_rgb = entity.attributes.get(light.ATTR_RGB_COLOR)
if color_rgb is not None:
query_response["color"]["spectrumRGB"] = \
int(color.color_rgb_to_hex(
color_rgb[0], color_rgb[1], color_rgb[2]), 16)
return query_response
# erroneous bug on old pythons and pylint
# https://github.com/PyCQA/pylint/issues/1212
# pylint: disable=invalid-sequence-index
def determine_service(entity_id: str, command: str,
params: dict) -> Tuple[str, dict]:
def determine_service(
entity_id: str, command: str, params: dict,
units: UnitSystem) -> Tuple[str, dict]:
"""
Determine service and service_data.
@@ -166,14 +238,17 @@ def determine_service(entity_id: str, command: str,
# special climate handling
if domain == climate.DOMAIN:
if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT:
service_data['temperature'] = params.get(
'thermostatTemperatureSetpoint', 25)
service_data['temperature'] = units.temperature(
params.get('thermostatTemperatureSetpoint', 25),
TEMP_CELSIUS)
return (climate.SERVICE_SET_TEMPERATURE, service_data)
if command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE:
service_data['target_temp_high'] = params.get(
'thermostatTemperatureSetpointHigh', 25)
service_data['target_temp_low'] = params.get(
'thermostatTemperatureSetpointLow', 18)
service_data['target_temp_high'] = units.temperature(
params.get('thermostatTemperatureSetpointHigh', 25),
TEMP_CELSIUS)
service_data['target_temp_low'] = units.temperature(
params.get('thermostatTemperatureSetpointLow', 18),
TEMP_CELSIUS)
return (climate.SERVICE_SET_TEMPERATURE, service_data)
if command == COMMAND_THERMOSTAT_SET_MODE:
service_data['operation_mode'] = params.get(
@@ -185,7 +260,27 @@ def determine_service(entity_id: str, command: str,
service_data['brightness'] = int(brightness / 100 * 255)
return (SERVICE_TURN_ON, service_data)
if command == COMMAND_ACTIVATESCENE or (COMMAND_ONOFF == command and
params.get('on') is True):
_LOGGER.debug("Handling command %s with data %s", command, params)
if command == COMMAND_COLOR:
color_data = params.get('color')
if color_data is not None:
if color_data.get('temperature', 0) > 0:
service_data[light.ATTR_KELVIN] = color_data.get('temperature')
return (SERVICE_TURN_ON, service_data)
if color_data.get('spectrumRGB', 0) > 0:
# blue is 255 so pad up to 6 chars
hex_value = \
('%0x' % int(color_data.get('spectrumRGB'))).zfill(6)
service_data[light.ATTR_RGB_COLOR] = \
color.rgb_hex_to_rgb_list(hex_value)
return (SERVICE_TURN_ON, service_data)
if command == COMMAND_ACTIVATESCENE:
return (SERVICE_TURN_ON, service_data)
return (SERVICE_TURN_OFF, service_data)
if COMMAND_ONOFF == command:
if params.get('on') is True:
return (SERVICE_TURN_ON, service_data)
return (SERVICE_TURN_OFF, service_data)
return (None, service_data)
+1 -1
View File
@@ -49,7 +49,7 @@ NO_TIMEOUT = {
}
NO_AUTH = {
re.compile(r'^panel$'), re.compile(r'^addons/[^/]*/logo$')
re.compile(r'^panel_(es5|latest)$'), re.compile(r'^addons/[^/]*/logo$')
}
SCHEMA_ADDON = vol.Schema({
+26 -19
View File
@@ -5,37 +5,43 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/http/
"""
import asyncio
import json
from functools import wraps
import logging
import ssl
from ipaddress import ip_network
import json
import logging
import os
import voluptuous as vol
from aiohttp import web
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPMovedPermanently
import ssl
from aiohttp import web
from aiohttp.hdrs import ACCEPT, ORIGIN, CONTENT_TYPE
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPMovedPermanently
import voluptuous as vol
from homeassistant.const import (
SERVER_PORT, CONTENT_TYPE_JSON, HTTP_HEADER_HA_AUTH,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
HTTP_HEADER_X_REQUESTED_WITH)
from homeassistant.core import is_callback
import homeassistant.helpers.config_validation as cv
import homeassistant.remote as rem
import homeassistant.util as hass_util
from homeassistant.const import (
SERVER_PORT, CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
from homeassistant.core import is_callback
from homeassistant.util.logging import HideSensitiveDataFilter
from .auth import auth_middleware
from .ban import ban_middleware
from .const import (
KEY_USE_X_FORWARDED_FOR, KEY_TRUSTED_NETWORKS, KEY_BANS_ENABLED,
KEY_LOGIN_THRESHOLD, KEY_AUTHENTICATED)
KEY_BANS_ENABLED, KEY_AUTHENTICATED, KEY_LOGIN_THRESHOLD,
KEY_TRUSTED_NETWORKS, KEY_USE_X_FORWARDED_FOR)
from .static import (
staticresource_middleware, CachingFileResponse, CachingStaticResource)
CachingFileResponse, CachingStaticResource, staticresource_middleware)
from .util import get_real_ip
REQUIREMENTS = ['aiohttp_cors==0.5.3']
ALLOWED_CORS_HEADERS = [
ORIGIN, ACCEPT, HTTP_HEADER_X_REQUESTED_WITH, CONTENT_TYPE,
HTTP_HEADER_HA_AUTH]
DOMAIN = 'http'
CONF_API_PASSWORD = 'api_password'
@@ -176,8 +182,6 @@ class HomeAssistantWSGI(object):
use_x_forwarded_for, trusted_networks,
login_threshold, is_ban_enabled):
"""Initialize the WSGI Home Assistant server."""
import aiohttp_cors
middlewares = [auth_middleware, staticresource_middleware]
if is_ban_enabled:
@@ -200,6 +204,8 @@ class HomeAssistantWSGI(object):
self.server = None
if cors_origins:
import aiohttp_cors
self.cors = aiohttp_cors.setup(self.app, defaults={
host: aiohttp_cors.ResourceOptions(
allow_headers=ALLOWED_CORS_HEADERS,
@@ -256,7 +262,6 @@ class HomeAssistantWSGI(object):
resource = CachingStaticResource
else:
resource = web.StaticResource
self.app.router.register_resource(resource(url_path, path))
return
@@ -329,7 +334,9 @@ class HomeAssistantWSGI(object):
_LOGGER.error("Failed to create HTTP server at port %d: %s",
self.server_port, error)
self.app._frozen = False # pylint: disable=protected-access
# pylint: disable=protected-access
self.app._middlewares = tuple(self.app._prepare_middleware())
self.app._frozen = False
@asyncio.coroutine
def stop(self):
@@ -339,7 +346,7 @@ class HomeAssistantWSGI(object):
yield from self.server.wait_closed()
yield from self.app.shutdown()
if self._handler:
yield from self._handler.finish_connections(60.0)
yield from self._handler.shutdown(10)
yield from self.app.cleanup()
+27 -36
View File
@@ -5,6 +5,7 @@ import hmac
import logging
from aiohttp import hdrs
from aiohttp.web import middleware
from homeassistant.const import HTTP_HEADER_HA_AUTH
from .util import get_real_ip
@@ -15,47 +16,37 @@ DATA_API_PASSWORD = 'api_password'
_LOGGER = logging.getLogger(__name__)
@middleware
@asyncio.coroutine
def auth_middleware(app, handler):
def auth_middleware(request, handler):
"""Authenticate as middleware."""
# If no password set, just always set authenticated=True
if app['hass'].http.api_password is None:
@asyncio.coroutine
def no_auth_middleware_handler(request):
"""Auth middleware to approve all requests."""
request[KEY_AUTHENTICATED] = True
return handler(request)
return no_auth_middleware_handler
@asyncio.coroutine
def auth_middleware_handler(request):
"""Auth middleware to check authentication."""
# Auth code verbose on purpose
authenticated = False
if (HTTP_HEADER_HA_AUTH in request.headers and
validate_password(
request, request.headers[HTTP_HEADER_HA_AUTH])):
# A valid auth header has been set
authenticated = True
elif (DATA_API_PASSWORD in request.query and
validate_password(request, request.query[DATA_API_PASSWORD])):
authenticated = True
elif (hdrs.AUTHORIZATION in request.headers and
validate_authorization_header(request)):
authenticated = True
elif is_trusted_ip(request):
authenticated = True
request[KEY_AUTHENTICATED] = authenticated
if request.app['hass'].http.api_password is None:
request[KEY_AUTHENTICATED] = True
return handler(request)
return auth_middleware_handler
# Check authentication
authenticated = False
if (HTTP_HEADER_HA_AUTH in request.headers and
validate_password(
request, request.headers[HTTP_HEADER_HA_AUTH])):
# A valid auth header has been set
authenticated = True
elif (DATA_API_PASSWORD in request.query and
validate_password(request, request.query[DATA_API_PASSWORD])):
authenticated = True
elif (hdrs.AUTHORIZATION in request.headers and
validate_authorization_header(request)):
authenticated = True
elif is_trusted_ip(request):
authenticated = True
request[KEY_AUTHENTICATED] = authenticated
return handler(request)
def is_trusted_ip(request):
+19 -21
View File
@@ -6,6 +6,7 @@ from ipaddress import ip_address
import logging
import os
from aiohttp.web import middleware
from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized
import voluptuous as vol
@@ -32,35 +33,32 @@ SCHEMA_IP_BAN_ENTRY = vol.Schema({
})
@middleware
@asyncio.coroutine
def ban_middleware(app, handler):
def ban_middleware(request, handler):
"""IP Ban middleware."""
if not app[KEY_BANS_ENABLED]:
return handler
if not request.app[KEY_BANS_ENABLED]:
return (yield from handler(request))
if KEY_BANNED_IPS not in app:
hass = app['hass']
app[KEY_BANNED_IPS] = yield from hass.async_add_job(
if KEY_BANNED_IPS not in request.app:
hass = request.app['hass']
request.app[KEY_BANNED_IPS] = yield from hass.async_add_job(
load_ip_bans_config, hass.config.path(IP_BANS_FILE))
@asyncio.coroutine
def ban_middleware_handler(request):
"""Verify if IP is not banned."""
ip_address_ = get_real_ip(request)
# Verify if IP is not banned
ip_address_ = get_real_ip(request)
is_banned = any(ip_ban.ip_address == ip_address_
for ip_ban in request.app[KEY_BANNED_IPS])
is_banned = any(ip_ban.ip_address == ip_address_
for ip_ban in request.app[KEY_BANNED_IPS])
if is_banned:
raise HTTPForbidden()
if is_banned:
raise HTTPForbidden()
try:
return (yield from handler(request))
except HTTPUnauthorized:
yield from process_wrong_login(request)
raise
return ban_middleware_handler
try:
return (yield from handler(request))
except HTTPUnauthorized:
yield from process_wrong_login(request)
raise
@asyncio.coroutine
+12 -15
View File
@@ -3,7 +3,7 @@ import asyncio
import re
from aiohttp import hdrs
from aiohttp.web import FileResponse
from aiohttp.web import FileResponse, middleware
from aiohttp.web_exceptions import HTTPNotFound
from aiohttp.web_urldispatcher import StaticResource
from yarl import unquote
@@ -61,21 +61,18 @@ class CachingFileResponse(FileResponse):
self._sendfile = sendfile
@middleware
@asyncio.coroutine
def staticresource_middleware(app, handler):
def staticresource_middleware(request, handler):
"""Middleware to strip out fingerprint from fingerprinted assets."""
@asyncio.coroutine
def static_middleware_handler(request):
"""Strip out fingerprints from resource names."""
if not request.path.startswith('/static/'):
return handler(request)
fingerprinted = _FINGERPRINT.match(request.match_info['filename'])
if fingerprinted:
request.match_info['filename'] = \
'{}.{}'.format(*fingerprinted.groups())
path = request.path
if not path.startswith('/static/') and not path.startswith('/frontend'):
return handler(request)
return static_middleware_handler
fingerprinted = _FINGERPRINT.match(request.match_info['filename'])
if fingerprinted:
request.match_info['filename'] = \
'{}.{}'.format(*fingerprinted.groups())
return handler(request)
+6 -4
View File
@@ -5,9 +5,9 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/influxdb/
"""
import logging
import re
import requests.exceptions
import voluptuous as vol
from homeassistant.const import (
@@ -123,10 +123,12 @@ def setup(hass, config):
try:
influx = InfluxDBClient(**kwargs)
influx.query("SHOW SERIES LIMIT 1;", database=conf[CONF_DB_NAME])
except exceptions.InfluxDBClientError as exc:
except (exceptions.InfluxDBClientError,
requests.exceptions.ConnectionError) as exc:
_LOGGER.error("Database host is not accessible due to '%s', please "
"check your entries in the configuration file and that "
"the database exists and is READ/WRITE.", exc)
"check your entries in the configuration file (host, "
"port, etc.) and verify that the database exists and is "
"READ/WRITE.", exc)
return False
def influx_event_listener(event):
+5 -5
View File
@@ -46,7 +46,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_HAS_DATE): cv.boolean,
vol.Required(CONF_HAS_TIME): cv.boolean,
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_INITIAL): cv.datetime,
vol.Optional(CONF_INITIAL): cv.string,
}, cv.has_at_least_one_key_value((CONF_HAS_DATE, True),
(CONF_HAS_TIME, True)))})
}, extra=vol.ALLOW_EXTRA)
@@ -137,15 +137,15 @@ class InputDatetime(Entity):
old_state = yield from async_get_last_state(self.hass,
self.entity_id)
if old_state is not None:
restore_val = dt_util.parse_datetime(old_state.state)
restore_val = old_state.state
if restore_val is not None:
if not self._has_date:
self._current_datetime = restore_val.time()
self._current_datetime = dt_util.parse_time(restore_val)
elif not self._has_time:
self._current_datetime = restore_val.date()
self._current_datetime = dt_util.parse_date(restore_val)
else:
self._current_datetime = restore_val
self._current_datetime = dt_util.parse_datetime(restore_val)
def has_date(self):
"""Return whether the input datetime carries a date."""
+1 -1
View File
@@ -36,7 +36,7 @@ ATTR_DISCOVER_DEVICES = 'devices'
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['xknx==0.7.16']
REQUIREMENTS = ['xknx==0.7.18']
TUNNELING_SCHEMA = vol.Schema({
vol.Required(CONF_HOST): cv.string,
+7 -27
View File
@@ -38,46 +38,26 @@ def setup(hass, config):
conf = config[DOMAIN]
hlmn = HassLaMetricManager(client_id=conf[CONF_CLIENT_ID],
client_secret=conf[CONF_CLIENT_SECRET])
devices = hlmn.manager().get_devices()
devices = hlmn.manager.get_devices()
if not devices:
_LOGGER.error("No LaMetric devices found")
return False
found = False
hass.data[DOMAIN] = hlmn
for dev in devices:
_LOGGER.debug("Discovered LaMetric device: %s", dev)
found = True
return found
return True
class HassLaMetricManager():
"""
A class that encapsulated requests to the LaMetric manager.
As the original class does not have a re-connect feature that is needed
for applications running for a long time as the OAuth tokens expire. This
class implements this reconnect() feature.
"""
"""A class that encapsulated requests to the LaMetric manager."""
def __init__(self, client_id, client_secret):
"""Initialize HassLaMetricManager and connect to LaMetric."""
from lmnotify import LaMetricManager
_LOGGER.debug("Connecting to LaMetric")
self.lmn = LaMetricManager(client_id, client_secret)
self.manager = LaMetricManager(client_id, client_secret)
self._client_id = client_id
self._client_secret = client_secret
def reconnect(self):
"""
Reconnect to LaMetric.
This is usually necessary when the OAuth token is expired.
"""
from lmnotify import LaMetricManager
_LOGGER.debug("Reconnecting to LaMetric")
self.lmn = LaMetricManager(self._client_id,
self._client_secret)
def manager(self):
"""Return the global LaMetricManager instance."""
return self.lmn
@@ -23,7 +23,6 @@ from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import async_restore_state
import homeassistant.util.color as color_util
DOMAIN = "light"
@@ -140,14 +139,6 @@ PROFILE_SCHEMA = vol.Schema(
_LOGGER = logging.getLogger(__name__)
def extract_info(state):
"""Extract light parameters from a state object."""
params = {key: state.attributes[key] for key in PROP_TO_ATTR
if key in state.attributes}
params['is_on'] = state.state == STATE_ON
return params
@bind_hass
def is_on(hass, entity_id=None):
"""Return if the lights are on based on the statemachine."""
@@ -431,9 +422,3 @@ class Light(ToggleEntity):
def supported_features(self):
"""Flag supported features."""
return 0
@asyncio.coroutine
def async_added_to_hass(self):
"""Component added, restore_state using platforms."""
if hasattr(self, 'async_restore_state'):
yield from async_restore_state(self, extract_info)
-24
View File
@@ -4,7 +4,6 @@ Demo light platform that implements lights.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
import asyncio
import random
from homeassistant.components.light import (
@@ -150,26 +149,3 @@ class DemoLight(Light):
# As we have disabled polling, we need to inform
# Home Assistant about updates in our state ourselves.
self.schedule_update_ha_state()
@asyncio.coroutine
def async_restore_state(self, is_on, **kwargs):
"""Restore the demo state."""
self._state = is_on
if 'brightness' in kwargs:
self._brightness = kwargs['brightness']
if 'color_temp' in kwargs:
self._ct = kwargs['color_temp']
if 'rgb_color' in kwargs:
self._rgb = kwargs['rgb_color']
if 'xy_color' in kwargs:
self._xy_color = kwargs['xy_color']
if 'white_value' in kwargs:
self._white = kwargs['white_value']
if 'effect' in kwargs:
self._effect = kwargs['effect']
@@ -4,6 +4,7 @@ Support for Lutron Caseta lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.lutron_caseta/
"""
import asyncio
import logging
from homeassistant.components.light import (
@@ -19,7 +20,8 @@ DEPENDENCIES = ['lutron_caseta']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Lutron Caseta lights."""
devs = []
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
@@ -28,7 +30,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev = LutronCasetaLight(light_device, bridge)
devs.append(dev)
add_devices(devs, True)
async_add_devices(devs, True)
class LutronCasetaLight(LutronCasetaDevice, Light):
@@ -44,7 +46,8 @@ class LutronCasetaLight(LutronCasetaDevice, Light):
"""Return the brightness of the light."""
return to_hass_level(self._state["current_state"])
def turn_on(self, **kwargs):
@asyncio.coroutine
def async_turn_on(self, **kwargs):
"""Turn the light on."""
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS]
@@ -53,7 +56,8 @@ class LutronCasetaLight(LutronCasetaDevice, Light):
self._smartbridge.set_value(self._device_id,
to_lutron_level(brightness))
def turn_off(self, **kwargs):
@asyncio.coroutine
def async_turn_off(self, **kwargs):
"""Turn the light off."""
self._smartbridge.set_value(self._device_id, 0)
@@ -62,7 +66,8 @@ class LutronCasetaLight(LutronCasetaDevice, Light):
"""Return true if device is on."""
return self._state["current_state"] > 0
def update(self):
@asyncio.coroutine
def async_update(self):
"""Call when forcing a refresh of the device."""
self._state = self._smartbridge.get_device_by_id(self._device_id)
_LOGGER.debug(self._state)
+9 -11
View File
@@ -4,15 +4,13 @@ Support for Tellstick lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.tellstick/
"""
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
from homeassistant.components.tellstick import (
DEFAULT_SIGNAL_REPETITIONS, ATTR_DISCOVER_DEVICES, ATTR_DISCOVER_CONFIG,
DOMAIN, TellstickDevice)
DATA_TELLSTICK, TellstickDevice)
PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): DOMAIN})
SUPPORT_TELLSTICK = SUPPORT_BRIGHTNESS
@@ -27,17 +25,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
signal_repetitions = discovery_info.get(
ATTR_DISCOVER_CONFIG, DEFAULT_SIGNAL_REPETITIONS)
add_devices(TellstickLight(tellcore_id, hass.data['tellcore_registry'],
signal_repetitions)
for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES])
add_devices([TellstickLight(hass.data[DATA_TELLSTICK][tellcore_id],
signal_repetitions)
for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES]],
True)
class TellstickLight(TellstickDevice, Light):
"""Representation of a Tellstick light."""
def __init__(self, tellcore_id, tellcore_registry, signal_repetitions):
def __init__(self, tellcore_device, signal_repetitions):
"""Initialize the Tellstick light."""
super().__init__(tellcore_id, tellcore_registry, signal_repetitions)
super().__init__(tellcore_device, signal_repetitions)
self._brightness = 255
@@ -57,9 +56,8 @@ class TellstickLight(TellstickDevice, Light):
def _parse_tellcore_data(self, tellcore_data):
"""Turn the value received from tellcore into something useful."""
if tellcore_data is not None:
brightness = int(tellcore_data)
return brightness
if tellcore_data:
return int(tellcore_data) # brightness
return None
def _update_model(self, new_state, data):
+7 -8
View File
@@ -8,6 +8,7 @@ import asyncio
import logging
from homeassistant.core import callback
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION,
SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP,
@@ -181,14 +182,12 @@ class TradfriLight(Light):
def device_state_attributes(self):
"""Return the devices' state attributes."""
info = self._light.device_info
attrs = {
'manufacturer': info.manufacturer,
'model_number': info.model_number,
'serial': info.serial,
'firmware_version': info.firmware_version,
'power_source': info.power_source_str,
'battery_level': info.battery_level
}
attrs = {}
if info.battery_level is not None:
attrs[ATTR_BATTERY_LEVEL] = info.battery_level
return attrs
@asyncio.coroutine
@@ -28,7 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
REQUIREMENTS = ['python-miio==0.3.0']
REQUIREMENTS = ['python-miio==0.3.1']
# The light does not accept cct values < 1
CCT_MIN = 1
@@ -64,14 +64,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
light = PhilipsEyecare(host, token)
device = XiaomiPhilipsEyecareLamp(name, light, device_info)
devices.append(device)
elif device_info.model == 'philips.light.ceil':
elif device_info.model == 'philips.light.ceiling':
from miio import Ceil
light = Ceil(host, token)
device = XiaomiPhilipsCeilingLamp(name, light, device_info)
devices.append(device)
elif device_info.model == 'philips.light.bulb':
from miio import Ceil
light = Ceil(host, token)
from miio import PhilipsBulb
light = PhilipsBulb(host, token)
device = XiaomiPhilipsLightBall(name, light, device_info)
devices.append(device)
else:
+24 -14
View File
@@ -14,7 +14,7 @@ from homeassistant.const import CONF_HOST
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['pylutron-caseta==0.2.8']
REQUIREMENTS = ['pylutron-caseta==0.3.0']
_LOGGER = logging.getLogger(__name__)
@@ -22,9 +22,16 @@ LUTRON_CASETA_SMARTBRIDGE = 'lutron_smartbridge'
DOMAIN = 'lutron_caseta'
CONF_KEYFILE = 'keyfile'
CONF_CERTFILE = 'certfile'
CONF_CA_CERTS = 'ca_certs'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_KEYFILE): cv.string,
vol.Required(CONF_CERTFILE): cv.string,
vol.Required(CONF_CA_CERTS): cv.string
})
}, extra=vol.ALLOW_EXTRA)
@@ -33,14 +40,21 @@ LUTRON_CASETA_COMPONENTS = [
]
def setup(hass, base_config):
@asyncio.coroutine
def async_setup(hass, base_config):
"""Set up the Lutron component."""
from pylutron_caseta.smartbridge import Smartbridge
config = base_config.get(DOMAIN)
hass.data[LUTRON_CASETA_SMARTBRIDGE] = Smartbridge(
hostname=config[CONF_HOST]
)
keyfile = hass.config.path(config[CONF_KEYFILE])
certfile = hass.config.path(config[CONF_CERTFILE])
ca_certs = hass.config.path(config[CONF_CA_CERTS])
bridge = Smartbridge.create_tls(hostname=config[CONF_HOST],
keyfile=keyfile,
certfile=certfile,
ca_certs=ca_certs)
hass.data[LUTRON_CASETA_SMARTBRIDGE] = bridge
yield from bridge.connect()
if not hass.data[LUTRON_CASETA_SMARTBRIDGE].is_connected():
_LOGGER.error("Unable to connect to Lutron smartbridge at %s",
config[CONF_HOST])
@@ -49,7 +63,8 @@ def setup(hass, base_config):
_LOGGER.info("Connected to Lutron smartbridge at %s", config[CONF_HOST])
for component in LUTRON_CASETA_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config)
hass.async_add_job(discovery.async_load_platform(hass, component,
DOMAIN, {}, config))
return True
@@ -73,13 +88,8 @@ class LutronCasetaDevice(Entity):
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
self.hass.async_add_job(
self._smartbridge.add_subscriber, self._device_id,
self._update_callback
)
def _update_callback(self):
self.schedule_update_ha_state()
self._smartbridge.add_subscriber(self._device_id,
self.async_schedule_update_ha_state)
@property
def name(self):
+1 -1
View File
@@ -16,7 +16,7 @@ from homeassistant.components.media_player import (
from homeassistant.config import load_yaml_config_file
from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['youtube_dl==2017.10.29']
REQUIREMENTS = ['youtube_dl==2017.11.15']
_LOGGER = logging.getLogger(__name__)
@@ -7,32 +7,33 @@ https://home-assistant.io/components/media_player/
import asyncio
from datetime import timedelta
import functools as ft
import collections
import hashlib
import logging
import os
from random import SystemRandom
from aiohttp import web, hdrs
from aiohttp import web
from aiohttp.hdrs import CONTENT_TYPE, CACHE_CONTROL
import async_timeout
import voluptuous as vol
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
from homeassistant.config import load_yaml_config_file
from homeassistant.loader import bind_hass
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
from homeassistant.const import (
STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, ATTR_ENTITY_ID,
SERVICE_TOGGLE, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP,
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_SEEK, SERVICE_MEDIA_STOP,
SERVICE_VOLUME_SET, SERVICE_MEDIA_PAUSE, SERVICE_SHUFFLE_SET,
SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.loader import bind_hass
from homeassistant.util.async import run_coroutine_threadsafe
from homeassistant.const import (
STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE,
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET,
SERVICE_VOLUME_MUTE, SERVICE_TOGGLE, SERVICE_MEDIA_STOP,
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK,
SERVICE_SHUFFLE_SET)
_LOGGER = logging.getLogger(__name__)
_RND = SystemRandom()
@@ -44,17 +45,16 @@ SCAN_INTERVAL = timedelta(seconds=10)
ENTITY_ID_FORMAT = DOMAIN + '.{}'
ENTITY_IMAGE_URL = '/api/media_player_proxy/{0}?token={1}&cache={2}'
ATTR_CACHE_IMAGES = 'images'
ATTR_CACHE_URLS = 'urls'
ATTR_CACHE_MAXSIZE = 'maxsize'
CACHE_IMAGES = 'images'
CACHE_MAXSIZE = 'maxsize'
CACHE_LOCK = 'lock'
CACHE_URL = 'url'
CACHE_CONTENT = 'content'
ENTITY_IMAGE_CACHE = {
ATTR_CACHE_IMAGES: {},
ATTR_CACHE_URLS: [],
ATTR_CACHE_MAXSIZE: 16
CACHE_IMAGES: collections.OrderedDict(),
CACHE_MAXSIZE: 16
}
CONTENT_TYPE_HEADER = 'Content-Type'
SERVICE_PLAY_MEDIA = 'play_media'
SERVICE_SELECT_SOURCE = 'select_source'
SERVICE_CLEAR_PLAYLIST = 'clear_playlist'
@@ -896,43 +896,36 @@ def _async_fetch_image(hass, url):
Images are cached in memory (the images are typically 10-100kB in size).
"""
cache_images = ENTITY_IMAGE_CACHE[ATTR_CACHE_IMAGES]
cache_urls = ENTITY_IMAGE_CACHE[ATTR_CACHE_URLS]
cache_maxsize = ENTITY_IMAGE_CACHE[ATTR_CACHE_MAXSIZE]
cache_images = ENTITY_IMAGE_CACHE[CACHE_IMAGES]
cache_maxsize = ENTITY_IMAGE_CACHE[CACHE_MAXSIZE]
if url in cache_images:
return cache_images[url]
if url not in cache_images:
cache_images[url] = {CACHE_LOCK: asyncio.Lock(loop=hass.loop)}
content, content_type = (None, None)
websession = async_get_clientsession(hass)
try:
with async_timeout.timeout(10, loop=hass.loop):
response = yield from websession.get(url)
with (yield from cache_images[url][CACHE_LOCK]):
if CACHE_CONTENT in cache_images[url]:
return cache_images[url][CACHE_CONTENT]
if response.status == 200:
content = yield from response.read()
content_type = response.headers.get(CONTENT_TYPE_HEADER)
if content_type:
content_type = content_type.split(';')[0]
content, content_type = (None, None)
websession = async_get_clientsession(hass)
try:
with async_timeout.timeout(10, loop=hass.loop):
response = yield from websession.get(url)
except asyncio.TimeoutError:
pass
if response.status == 200:
content = yield from response.read()
content_type = response.headers.get(CONTENT_TYPE)
if content_type:
content_type = content_type.split(';')[0]
cache_images[url][CACHE_CONTENT] = content, content_type
if not content:
return (None, None)
except asyncio.TimeoutError:
pass
cache_images[url] = (content, content_type)
cache_urls.append(url)
while len(cache_images) > cache_maxsize:
cache_images.popitem(last=False)
while len(cache_urls) > cache_maxsize:
# remove oldest item from cache
oldest_url = cache_urls[0]
if oldest_url in cache_images:
del cache_images[oldest_url]
cache_urls = cache_urls[1:]
return content, content_type
return content, content_type
class MediaPlayerImageView(HomeAssistantView):
@@ -965,8 +958,6 @@ class MediaPlayerImageView(HomeAssistantView):
if data is None:
return web.Response(status=500)
headers = {hdrs.CACHE_CONTROL: 'max-age=3600'}
headers = {CACHE_CONTROL: 'max-age=3600'}
return web.Response(
body=data,
content_type=content_type,
headers=headers)
body=data, content_type=content_type, headers=headers)
@@ -4,33 +4,37 @@ Bluesound.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.bluesound/
"""
import logging
from datetime import timedelta
from asyncio.futures import CancelledError
import asyncio
import voluptuous as vol
from aiohttp.client_exceptions import ClientError
from asyncio.futures import CancelledError
from datetime import timedelta
import logging
import aiohttp
from aiohttp.client_exceptions import ClientError
from aiohttp.hdrs import CONNECTION, KEEP_ALIVE
import async_timeout
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.core import callback
from homeassistant.util import Throttle
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.util.dt as dt_util
import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC,
SUPPORT_CLEAR_PLAYLIST, SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_STEP)
SUPPORT_PLAY, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PAUSE, PLATFORM_SCHEMA,
MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA,
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_PREVIOUS_TRACK,
MediaPlayerDevice)
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
STATE_PLAYING, STATE_PAUSED, STATE_IDLE, CONF_HOSTS,
CONF_HOST, CONF_PORT, CONF_NAME)
CONF_HOST, CONF_NAME, CONF_PORT, CONF_HOSTS, STATE_IDLE, STATE_PAUSED,
STATE_PLAYING, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util import Throttle
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['xmltodict==0.11.0']
_LOGGER = logging.getLogger(__name__)
STATE_OFFLINE = 'offline'
ATTR_MODEL = 'model'
ATTR_MODEL_NAME = 'model_name'
@@ -46,8 +50,6 @@ UPDATE_PRESETS_INTERVAL = timedelta(minutes=30)
NODE_OFFLINE_CHECK_TIMEOUT = 180
NODE_RETRY_INITIATION = timedelta(minutes=3)
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [{
vol.Required(CONF_HOST): cv.string,
@@ -80,20 +82,15 @@ def _add_player(hass, async_add_devices, host, port=None, name=None):
def _add_player_cb():
"""Add player after first sync fetch."""
async_add_devices([player])
_LOGGER.info('Added Bluesound device with name: %s', player.name)
_LOGGER.info("Added device with name: %s", player.name)
if hass.is_running:
_start_polling()
else:
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START,
_start_polling
)
EVENT_HOMEASSISTANT_START, _start_polling)
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP,
_stop_polling
)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_polling)
player = BluesoundPlayer(hass, host, port, name, _add_player_cb)
hass.data[DATA_BLUESOUND].append(player)
@@ -101,10 +98,7 @@ def _add_player(hass, async_add_devices, host, port=None, name=None):
if hass.is_running:
_init_player()
else:
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START,
_init_player
)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _init_player)
@asyncio.coroutine
@@ -121,11 +115,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
hosts = config.get(CONF_HOSTS, None)
if hosts:
for host in hosts:
_add_player(hass,
async_add_devices,
host.get(CONF_HOST),
host.get(CONF_PORT, None),
host.get(CONF_NAME, None))
_add_player(
hass, async_add_devices, host.get(CONF_HOST),
host.get(CONF_PORT), host.get(CONF_NAME, None))
class BluesoundPlayer(MediaPlayerDevice):
@@ -137,7 +129,7 @@ class BluesoundPlayer(MediaPlayerDevice):
self._hass = hass
self._port = port
self._polling_session = async_get_clientsession(hass)
self._polling_task = None # The actuall polling task.
self._polling_task = None # The actual polling task.
self._name = name
self._brand = None
self._model = None
@@ -156,7 +148,6 @@ class BluesoundPlayer(MediaPlayerDevice):
if self._port is None:
self._port = DEFAULT_PORT
# Internal methods
@staticmethod
def _try_get_index(string, seach_string):
try:
@@ -165,13 +156,12 @@ class BluesoundPlayer(MediaPlayerDevice):
return -1
@asyncio.coroutine
def _internal_update_sync_status(self, on_updated_cb=None,
raise_timeout=False):
def _internal_update_sync_status(
self, on_updated_cb=None, raise_timeout=False):
resp = None
try:
resp = yield from self.send_bluesound_command(
'SyncStatus',
raise_timeout, raise_timeout)
'SyncStatus', raise_timeout, raise_timeout)
except:
raise
@@ -193,9 +183,7 @@ class BluesoundPlayer(MediaPlayerDevice):
if on_updated_cb:
on_updated_cb()
return True
# END Internal methods
# Poll functionality
@asyncio.coroutine
def _start_poll_command(self):
""""Loop which polls the status of the player."""
@@ -204,14 +192,13 @@ class BluesoundPlayer(MediaPlayerDevice):
yield from self.async_update_status()
except (asyncio.TimeoutError, ClientError):
_LOGGER.info("Bluesound node %s is offline, retrying later",
self._name)
yield from asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT,
loop=self._hass.loop)
_LOGGER.info("Node %s is offline, retrying later", self._name)
yield from asyncio.sleep(
NODE_OFFLINE_CHECK_TIMEOUT, loop=self._hass.loop)
self.start_polling()
except CancelledError:
_LOGGER.debug("Stopping bluesound polling of node %s", self._name)
_LOGGER.debug("Stopping the polling of node %s", self._name)
except:
_LOGGER.exception("Unexpected error in %s", self._name)
raise
@@ -224,9 +211,7 @@ class BluesoundPlayer(MediaPlayerDevice):
def stop_polling(self):
"""Stop the polling task."""
self._polling_task.cancel()
# END Poll functionality
# Initiator
@asyncio.coroutine
def async_init(self):
"""Initiate the player async."""
@@ -235,22 +220,17 @@ class BluesoundPlayer(MediaPlayerDevice):
self._retry_remove()
self._retry_remove = None
yield from self._internal_update_sync_status(self._init_callback,
True)
yield from self._internal_update_sync_status(
self._init_callback, True)
except (asyncio.TimeoutError, ClientError):
_LOGGER.info("Bluesound node %s is offline, retrying later",
self.host)
_LOGGER.info("Node %s is offline, retrying later", self.host)
self._retry_remove = async_track_time_interval(
self._hass,
self.async_init,
NODE_RETRY_INITIATION)
self._hass, self.async_init, NODE_RETRY_INITIATION)
except:
_LOGGER.exception("Unexpected when initiating error in %s",
self.host)
raise
# END Initiator
# Status updates fetchers
@asyncio.coroutine
def async_update(self):
"""Update internal status of the entity."""
@@ -275,7 +255,7 @@ class BluesoundPlayer(MediaPlayerDevice):
method = method[1:]
url = "http://{}:{}/{}".format(self.host, self._port, method)
_LOGGER.info("calling URL: %s", url)
_LOGGER.debug("Calling URL: %s", url)
response = None
try:
websession = async_get_clientsession(self._hass)
@@ -294,11 +274,10 @@ class BluesoundPlayer(MediaPlayerDevice):
except (asyncio.TimeoutError, aiohttp.ClientError):
if raise_timeout:
_LOGGER.info("Timeout with Bluesound: %s", self.host)
_LOGGER.info("Timeout: %s", self.host)
raise
else:
_LOGGER.debug("Failed communicating with Bluesound: %s",
self.host)
_LOGGER.debug("Failed communicating: %s", self.host)
return None
return data
@@ -315,17 +294,17 @@ class BluesoundPlayer(MediaPlayerDevice):
etag = self._status.get('@etag', '')
if etag != '':
url = 'Status?etag='+etag+'&timeout=60.0'
url = 'Status?etag={}&timeout=60.0'.format(etag)
url = "http://{}:{}/{}".format(self.host, self._port, url)
_LOGGER.debug("calling URL: %s", url)
_LOGGER.debug("Calling URL: %s", url)
try:
with async_timeout.timeout(65, loop=self._hass.loop):
response = yield from self._polling_session.get(
url,
headers={'connection': 'keep-alive'})
headers={CONNECTION: KEEP_ALIVE})
if response.status != 200:
_LOGGER.error("Error %s on %s", response.status, url)
@@ -350,8 +329,8 @@ class BluesoundPlayer(MediaPlayerDevice):
def async_update_sync_status(self, on_updated_cb=None,
raise_timeout=False):
"""Update sync status."""
yield from self._internal_update_sync_status(on_updated_cb,
raise_timeout=False)
yield from self._internal_update_sync_status(
on_updated_cb, raise_timeout=False)
@asyncio.coroutine
@Throttle(UPDATE_CAPTURE_INTERVAL)
@@ -436,9 +415,7 @@ class BluesoundPlayer(MediaPlayerDevice):
_create_service_item(resp['services']['service'])
return self._services_items
# END Status updates fetchers
# Media player (and core) properties
@property
def should_poll(self):
"""No need to poll information."""
@@ -611,17 +588,17 @@ class BluesoundPlayer(MediaPlayerDevice):
stream_url = self._status.get('streamUrl', '')
if self._status.get('is_preset', '') == '1' and stream_url != '':
# this check doesn't work with all presets, for example playlists.
# But it works with radio service_items will catch playlists
# This check doesn't work with all presets, for example playlists.
# But it works with radio service_items will catch playlists.
items = [x for x in self._preset_items if 'url2' in x and
parse.unquote(x['url2']) == stream_url]
if len(items) > 0:
return items[0]['title']
# this could be a bit difficult to detect. Bluetooth could be named
# This could be a bit difficult to detect. Bluetooth could be named
# different things and there is not any way to match chooses in
# capture list to current playing. It's a bit of guesswork.
# This method will be needing some tweaking over time
# This method will be needing some tweaking over time.
title = self._status.get('title1', '').lower()
if title == 'bluetooth' or stream_url == 'Capture:hw:2,0/44100/16/2':
items = [x for x in self._capture_items
@@ -660,7 +637,7 @@ class BluesoundPlayer(MediaPlayerDevice):
return items[0]['title']
if self._status.get('streamUrl', '') != '':
_LOGGER.debug("Couldn't find source of stream url: %s",
_LOGGER.debug("Couldn't find source of stream URL: %s",
self._status.get('streamUrl', ''))
return None
@@ -695,9 +672,7 @@ class BluesoundPlayer(MediaPlayerDevice):
ATTR_MODEL_NAME: self._model_name,
ATTR_BRAND: self._brand,
}
# END Media player (and core) properties
# Media player commands
@asyncio.coroutine
def async_select_source(self, source):
"""Select input source."""
@@ -712,8 +687,8 @@ class BluesoundPlayer(MediaPlayerDevice):
return
selected_source = items[0]
url = 'Play?url={}&preset_id&image={}'.format(selected_source['url'],
selected_source['image'])
url = 'Play?url={}&preset_id&image={}'.format(
selected_source['url'], selected_source['image'])
if 'is_raw_url' in selected_source and selected_source['is_raw_url']:
url = selected_source['url']
@@ -806,4 +781,3 @@ class BluesoundPlayer(MediaPlayerDevice):
else:
return self.send_bluesound_command(
'Volume?level=' + str(float(self._lastvol) * 100))
# END Media player commands
@@ -20,7 +20,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['snapcast==2.0.7']
REQUIREMENTS = ['snapcast==2.0.8']
_LOGGER = logging.getLogger(__name__)
@@ -80,7 +80,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
try:
server = yield from snapcast.control.create_server(
hass.loop, host, port)
hass.loop, host, port, reconnect=True)
except socket.gaierror:
_LOGGER.error('Could not connect to Snapcast server at %s:%d',
host, port)
@@ -9,28 +9,30 @@ import logging
# pylint: disable=import-error
from copy import copy
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.media_player import (
ATTR_APP_ID, ATTR_APP_NAME, ATTR_MEDIA_ALBUM_ARTIST, ATTR_MEDIA_ALBUM_NAME,
ATTR_MEDIA_ARTIST, ATTR_MEDIA_CHANNEL, ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION, ATTR_MEDIA_EPISODE,
ATTR_MEDIA_PLAYLIST, ATTR_MEDIA_SEASON, ATTR_MEDIA_SEEK_POSITION,
ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_TITLE, ATTR_MEDIA_TRACK,
ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_INPUT_SOURCE_LIST,
ATTR_MEDIA_POSITION, ATTR_MEDIA_SHUFFLE,
ATTR_MEDIA_POSITION_UPDATED_AT, DOMAIN, SERVICE_PLAY_MEDIA,
ATTR_APP_ID, ATTR_APP_NAME, ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST,
ATTR_MEDIA_ALBUM_ARTIST, ATTR_MEDIA_ALBUM_NAME, ATTR_MEDIA_ARTIST,
ATTR_MEDIA_CHANNEL, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE,
ATTR_MEDIA_DURATION, ATTR_MEDIA_EPISODE, ATTR_MEDIA_PLAYLIST,
ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT, ATTR_MEDIA_SEASON,
ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_SHUFFLE,
ATTR_MEDIA_TITLE, ATTR_MEDIA_TRACK, ATTR_MEDIA_VOLUME_LEVEL,
ATTR_MEDIA_VOLUME_MUTED, DOMAIN, MediaPlayerDevice, PLATFORM_SCHEMA,
SERVICE_CLEAR_PLAYLIST, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE,
SUPPORT_CLEAR_PLAYLIST, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST,
SUPPORT_SHUFFLE_SET, ATTR_INPUT_SOURCE, SERVICE_SELECT_SOURCE,
SERVICE_CLEAR_PLAYLIST, MediaPlayerDevice)
SUPPORT_VOLUME_STEP)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, CONF_NAME, SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE,
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF,
SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, SERVICE_SHUFFLE_SET, STATE_IDLE,
STATE_OFF, STATE_ON, SERVICE_MEDIA_STOP, ATTR_SUPPORTED_FEATURES)
from homeassistant.helpers.event import async_track_state_change
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_FEATURES, CONF_NAME,
CONF_STATE_TEMPLATE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN,
SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP,
SERVICE_SHUFFLE_SET, STATE_IDLE, STATE_OFF, STATE_ON, SERVICE_MEDIA_STOP)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import async_call_from_config
ATTR_ACTIVE_CHILD = 'active_child'
@@ -48,113 +50,75 @@ OFF_STATES = [STATE_IDLE, STATE_OFF]
REQUIREMENTS = []
_LOGGER = logging.getLogger(__name__)
ATTRS_SCHEMA = vol.Schema({cv.slug: cv.string})
CMD_SCHEMA = vol.Schema({cv.slug: cv.SERVICE_SCHEMA})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_CHILDREN, default=[]): cv.entity_ids,
vol.Optional(CONF_COMMANDS, default={}): CMD_SCHEMA,
vol.Optional(CONF_ATTRS, default={}):
vol.Or(cv.ensure_list(ATTRS_SCHEMA), ATTRS_SCHEMA),
vol.Optional(CONF_STATE_TEMPLATE): cv.template
}, extra=vol.REMOVE_EXTRA)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the universal media players."""
if not validate_config(config):
return
player = UniversalMediaPlayer(
hass,
config[CONF_NAME],
config[CONF_CHILDREN],
config[CONF_COMMANDS],
config[CONF_ATTRS]
config.get(CONF_NAME),
config.get(CONF_CHILDREN),
config.get(CONF_COMMANDS),
config.get(CONF_ATTRS),
config.get(CONF_STATE_TEMPLATE)
)
async_add_devices([player])
def validate_config(config):
"""Validate universal media player configuration."""
del config[CONF_PLATFORM]
# Validate name
if CONF_NAME not in config:
_LOGGER.error("Universal Media Player configuration requires name")
return False
validate_children(config)
validate_commands(config)
validate_attributes(config)
del_keys = []
for key in config:
if key not in [CONF_NAME, CONF_CHILDREN, CONF_COMMANDS, CONF_ATTRS]:
_LOGGER.warning(
"Universal Media Player (%s) unrecognized parameter %s",
config[CONF_NAME], key)
del_keys.append(key)
for key in del_keys:
del config[key]
return True
def validate_children(config):
"""Validate children."""
if CONF_CHILDREN not in config:
_LOGGER.info(
"No children under Universal Media Player (%s)", config[CONF_NAME])
config[CONF_CHILDREN] = []
elif not isinstance(config[CONF_CHILDREN], list):
_LOGGER.warning(
"Universal Media Player (%s) children not list in config. "
"They will be ignored", config[CONF_NAME])
config[CONF_CHILDREN] = []
def validate_commands(config):
"""Validate commands."""
if CONF_COMMANDS not in config:
config[CONF_COMMANDS] = {}
elif not isinstance(config[CONF_COMMANDS], dict):
_LOGGER.warning(
"Universal Media Player (%s) specified commands not dict in "
"config. They will be ignored", config[CONF_NAME])
config[CONF_COMMANDS] = {}
def validate_attributes(config):
"""Validate attributes."""
if CONF_ATTRS not in config:
config[CONF_ATTRS] = {}
elif not isinstance(config[CONF_ATTRS], dict):
_LOGGER.warning(
"Universal Media Player (%s) specified attributes "
"not dict in config. They will be ignored", config[CONF_NAME])
config[CONF_ATTRS] = {}
for key, val in config[CONF_ATTRS].items():
attr = val.split('|', 1)
if len(attr) == 1:
attr.append(None)
config[CONF_ATTRS][key] = attr
class UniversalMediaPlayer(MediaPlayerDevice):
"""Representation of an universal media player."""
def __init__(self, hass, name, children, commands, attributes):
def __init__(self, hass, name, children,
commands, attributes, state_template=None):
"""Initialize the Universal media device."""
self.hass = hass
self._name = name
self._children = children
self._cmds = commands
self._attrs = attributes
self._attrs = {}
for key, val in attributes.items():
attr = val.split('|', 1)
if len(attr) == 1:
attr.append(None)
self._attrs[key] = attr
self._child_state = None
self._state_template = state_template
if state_template is not None:
self._state_template.hass = hass
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe to children and template state changes.
This method must be run in the event loop and returns a coroutine.
"""
@callback
def async_on_dependency_update(*_):
"""Update ha state when dependencies update."""
self.async_schedule_update_ha_state(True)
depend = copy(children)
for entity in attributes.values():
depend = copy(self._children)
for entity in self._attrs.values():
depend.append(entity[0])
if self._state_template is not None:
for entity in self._state_template.extract_entities():
depend.append(entity)
async_track_state_change(hass, depend, async_on_dependency_update)
self.hass.helpers.event.async_track_state_change(
list(set(depend)), async_on_dependency_update)
def _entity_lkp(self, entity_id, state_attr=None):
"""Look up an entity state."""
@@ -211,6 +175,8 @@ class UniversalMediaPlayer(MediaPlayerDevice):
@property
def master_state(self):
"""Return the master state for entity or None."""
if self._state_template is not None:
return self._state_template.async_render()
if CONF_STATE in self._attrs:
master_state = self._entity_lkp(
self._attrs[CONF_STATE][0], self._attrs[CONF_STATE][1])
@@ -232,8 +198,8 @@ class UniversalMediaPlayer(MediaPlayerDevice):
else master state or off
"""
master_state = self.master_state # avoid multiple lookups
if master_state == STATE_OFF:
return STATE_OFF
if (master_state == STATE_OFF) or (self._state_template is not None):
return master_state
active_child = self._child_state
if active_child:
@@ -57,7 +57,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA,
vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string,
vol.Optional(CONF_TIMEOUT, default=10): cv.positive_int,
vol.Optional(CONF_TIMEOUT, default=8): cv.positive_int,
vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
})
@@ -202,29 +202,25 @@ class LgWebOSDevice(MediaPlayerDevice):
for app in self._client.get_apps():
self._app_list[app['id']] = app
if conf_sources:
if app['id'] == self._current_source_id:
self._current_source = app['title']
self._source_list[app['title']] = app
elif (app['id'] in conf_sources or
any(word in app['title']
for word in conf_sources) or
any(word in app['id']
for word in conf_sources)):
self._source_list[app['title']] = app
else:
if app['id'] == self._current_source_id:
self._current_source = app['title']
self._source_list[app['title']] = app
elif (not conf_sources or
app['id'] in conf_sources or
any(word in app['title']
for word in conf_sources) or
any(word in app['id']
for word in conf_sources)):
self._source_list[app['title']] = app
for source in self._client.get_inputs():
if conf_sources:
if source['id'] == self._current_source_id:
self._source_list[source['label']] = source
elif (source['label'] in conf_sources or
any(source['label'].find(word) != -1
for word in conf_sources)):
self._source_list[source['label']] = source
else:
if source['id'] == self._current_source_id:
self._current_source = source['label']
self._source_list[source['label']] = source
elif (not conf_sources or
source['label'] in conf_sources or
any(source['label'].find(word) != -1
for word in conf_sources)):
self._source_list[source['label']] = source
except (OSError, ConnectionClosed, TypeError,
asyncio.TimeoutError):
@@ -10,10 +10,11 @@ media_player:
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.const import (
CONF_HOST, CONF_PORT,
STATE_UNKNOWN, STATE_ON
STATE_UNKNOWN, STATE_ON, STATE_PLAYING, STATE_PAUSED, STATE_IDLE
)
from homeassistant.components.media_player import (
MediaPlayerDevice, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA,
@@ -35,7 +36,7 @@ SUPPORTED_FEATURES = (
KNOWN_HOSTS_KEY = 'data_yamaha_musiccast'
INTERVAL_SECONDS = 'interval_seconds'
REQUIREMENTS = ['pymusiccast==0.1.3']
REQUIREMENTS = ['pymusiccast==0.1.5']
DEFAULT_PORT = 5005
DEFAULT_INTERVAL = 480
@@ -111,6 +112,7 @@ class YamahaDevice(MediaPlayerDevice):
self._zone = zone
self.mute = False
self.media_status = None
self.media_status_received = None
self.power = STATE_UNKNOWN
self.status = STATE_UNKNOWN
self.volume = 0
@@ -202,12 +204,34 @@ class YamahaDevice(MediaPlayerDevice):
"""Title of current playing media."""
return self.media_status.media_title if self.media_status else None
@property
def media_position(self):
"""Position of current playing media in seconds."""
if self.media_status and self.state in \
[STATE_PLAYING, STATE_PAUSED, STATE_IDLE]:
return self.media_status.media_position
@property
def media_position_updated_at(self):
"""When was the position of the current playing media valid.
Returns value from homeassistant.util.dt.utcnow().
"""
return self.media_status_received if self.media_status else None
def update(self):
"""Get the latest details from the device."""
_LOGGER.debug("update: %s", self.entity_id)
self._recv.update_status()
self._zone.update_status()
def update_hass(self):
"""Push updates to HASS."""
if self.entity_id:
_LOGGER.debug("update_hass: pushing updates")
self.schedule_update_ha_state()
return True
def turn_on(self):
"""Turn on specified media player or all."""
_LOGGER.debug("Turn device: on")
@@ -259,3 +283,9 @@ class YamahaDevice(MediaPlayerDevice):
_LOGGER.debug("select_source: %s", source)
self.status = STATE_UNKNOWN
self._zone.set_input(source)
def new_media_status(self, status):
"""Handle updates of the media status."""
_LOGGER.debug("new media_status arrived")
self.media_status = status
self.media_status_received = dt_util.utcnow()
+12 -22
View File
@@ -438,7 +438,8 @@ class MQTT(object):
self.broker = broker
self.port = port
self.keepalive = keepalive
self.topics = {}
self.wanted_topics = {}
self.subscribed_topics = {}
self.progress = {}
self.birth_message = birth_message
self._mqttc = None
@@ -526,15 +527,14 @@ class MQTT(object):
raise HomeAssistantError("topic need to be a string!")
with (yield from self._paho_lock):
if topic in self.topics:
if topic in self.subscribed_topics:
return
self.wanted_topics[topic] = qos
result, mid = yield from self.hass.async_add_job(
self._mqttc.subscribe, topic, qos)
_raise_on_error(result)
self.progress[mid] = topic
self.topics[topic] = None
@asyncio.coroutine
def async_unsubscribe(self, topic):
@@ -542,6 +542,7 @@ class MQTT(object):
This method is a coroutine.
"""
self.wanted_topics.pop(topic, None)
result, mid = yield from self.hass.async_add_job(
self._mqttc.unsubscribe, topic)
@@ -562,15 +563,10 @@ class MQTT(object):
self._mqttc.disconnect()
return
old_topics = self.topics
self.topics = {key: value for key, value in self.topics.items()
if value is None}
for topic, qos in old_topics.items():
# qos is None if we were in process of subscribing
if qos is not None:
self.hass.add_job(self.async_subscribe, topic, qos)
self.progress = {}
self.subscribed_topics = {}
for topic, qos in self.wanted_topics.items():
self.hass.add_job(self.async_subscribe, topic, qos)
if self.birth_message:
self.hass.add_job(self.async_publish(
@@ -584,7 +580,7 @@ class MQTT(object):
topic = self.progress.pop(mid, None)
if topic is None:
return
self.topics[topic] = granted_qos[0]
self.subscribed_topics[topic] = granted_qos[0]
def _mqtt_on_message(self, _mqttc, _userdata, msg):
"""Message received callback."""
@@ -598,18 +594,12 @@ class MQTT(object):
topic = self.progress.pop(mid, None)
if topic is None:
return
self.topics.pop(topic, None)
self.subscribed_topics.pop(topic, None)
def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code):
"""Disconnected callback."""
self.progress = {}
self.topics = {key: value for key, value in self.topics.items()
if value is not None}
# Remove None values from topic list
for key in list(self.topics):
if self.topics[key] is None:
self.topics.pop(key)
self.subscribed_topics = {}
# When disconnected because of calling disconnect()
if result_code == 0:
+3 -1
View File
@@ -20,10 +20,12 @@ TOPIC_MATCHER = re.compile(
r'(?P<prefix_topic>\w+)/(?P<component>\w+)/'
r'(?:(?P<node_id>[a-zA-Z0-9_-]+)/)?(?P<object_id>[a-zA-Z0-9_-]+)/config')
SUPPORTED_COMPONENTS = ['binary_sensor', 'fan', 'light', 'sensor', 'switch']
SUPPORTED_COMPONENTS = [
'binary_sensor', 'cover', 'fan', 'light', 'sensor', 'switch']
ALLOWED_PLATFORMS = {
'binary_sensor': ['mqtt'],
'cover': ['mqtt'],
'fan': ['mqtt'],
'light': ['mqtt', 'mqtt_json', 'mqtt_template'],
'sensor': ['mqtt'],
+1 -1
View File
@@ -13,7 +13,7 @@ import voluptuous as vol
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['hbmqtt==0.8']
REQUIREMENTS = ['hbmqtt==0.9.1']
DEPENDENCIES = ['http']
# None allows custom config to be created through generate_config
+23 -1
View File
@@ -9,9 +9,11 @@ import json
import voluptuous as vol
from homeassistant.const import MATCH_ALL
from homeassistant.const import (CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE,
CONF_INCLUDE, MATCH_ALL)
from homeassistant.core import callback
from homeassistant.components.mqtt import valid_publish_topic
from homeassistant.helpers.entityfilter import generate_filter
from homeassistant.helpers.event import async_track_state_change
from homeassistant.remote import JSONEncoder
import homeassistant.helpers.config_validation as cv
@@ -24,6 +26,16 @@ DOMAIN = 'mqtt_statestream'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
vol.Optional(CONF_DOMAINS, default=[]):
vol.All(cv.ensure_list, [cv.string])
}),
vol.Optional(CONF_INCLUDE, default={}): vol.Schema({
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
vol.Optional(CONF_DOMAINS, default=[]):
vol.All(cv.ensure_list, [cv.string])
}),
vol.Required(CONF_BASE_TOPIC): valid_publish_topic,
vol.Optional(CONF_PUBLISH_ATTRIBUTES, default=False): cv.boolean,
vol.Optional(CONF_PUBLISH_TIMESTAMPS, default=False): cv.boolean
@@ -36,8 +48,14 @@ def async_setup(hass, config):
"""Set up the MQTT state feed."""
conf = config.get(DOMAIN, {})
base_topic = conf.get(CONF_BASE_TOPIC)
pub_include = conf.get(CONF_INCLUDE, {})
pub_exclude = conf.get(CONF_EXCLUDE, {})
publish_attributes = conf.get(CONF_PUBLISH_ATTRIBUTES)
publish_timestamps = conf.get(CONF_PUBLISH_TIMESTAMPS)
publish_filter = generate_filter(pub_include.get(CONF_DOMAINS, []),
pub_include.get(CONF_ENTITIES, []),
pub_exclude.get(CONF_DOMAINS, []),
pub_exclude.get(CONF_ENTITIES, []))
if not base_topic.endswith('/'):
base_topic = base_topic + '/'
@@ -45,6 +63,10 @@ def async_setup(hass, config):
def _state_publisher(entity_id, old_state, new_state):
if new_state is None:
return
if not publish_filter(entity_id):
return
payload = new_state.state
mybase = base_topic + entity_id.replace('.', '/') + '/'
+1 -1
View File
@@ -90,7 +90,7 @@ def setup(hass, config):
_LOGGER.debug("Failed to login to Neato API")
return False
hub.update_robots()
for component in ('camera', 'sensor', 'switch'):
for component in ('camera', 'vacuum', 'switch'):
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True
+10 -7
View File
@@ -6,23 +6,26 @@ https://home-assistant.io/components/no_ip/
"""
import asyncio
import base64
import logging
from datetime import timedelta
import logging
import aiohttp
from aiohttp.hdrs import USER_AGENT, AUTHORIZATION
import async_timeout
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_DOMAIN, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME, HTTP_HEADER_AUTH,
HTTP_HEADER_USER_AGENT, PROJECT_EMAIL)
CONF_DOMAIN, CONF_TIMEOUT, CONF_PASSWORD, CONF_USERNAME)
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'no_ip'
# We should set a dedicated address for the user agent.
EMAIL = 'hello@home-assistant.io'
INTERVAL = timedelta(minutes=5)
DEFAULT_TIMEOUT = 10
@@ -38,7 +41,7 @@ NO_IP_ERRORS = {
}
UPDATE_URL = 'https://dynupdate.noip.com/nic/update'
USER_AGENT = "{} {}".format(SERVER_SOFTWARE, PROJECT_EMAIL)
HA_USER_AGENT = "{} {}".format(SERVER_SOFTWARE, EMAIL)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@@ -89,8 +92,8 @@ def _update_no_ip(hass, session, domain, auth_str, timeout):
}
headers = {
HTTP_HEADER_AUTH: "Basic {}".format(auth_str.decode('utf-8')),
HTTP_HEADER_USER_AGENT: USER_AGENT,
AUTHORIZATION: "Basic {}".format(auth_str.decode('utf-8')),
USER_AGENT: HA_USER_AGENT,
}
try:
+1 -1
View File
@@ -17,7 +17,7 @@ from homeassistant.const import CONF_NAME, CONF_PLATFORM
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import template as template_helper
REQUIREMENTS = ['apns2==0.1.1']
REQUIREMENTS = ['apns2==0.3.0']
APNS_DEVICES = 'apns.yaml'
CONF_CERTFILE = 'cert_file'
+6 -6
View File
@@ -6,22 +6,22 @@ https://home-assistant.io/components/notify.clicksend/
"""
import json
import logging
import requests
from aiohttp.hdrs import CONTENT_TYPE
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_USERNAME, CONF_API_KEY, CONF_RECIPIENT, HTTP_HEADER_CONTENT_TYPE,
CONTENT_TYPE_JSON)
from homeassistant.components.notify import (
PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import (
CONF_API_KEY, CONF_USERNAME, CONF_RECIPIENT, CONTENT_TYPE_JSON)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
BASE_API_URL = 'https://rest.clicksend.com/v3'
HEADERS = {HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_JSON}
HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
@@ -8,22 +8,22 @@ https://home-assistant.io/components/notify.clicksend_tts/
"""
import json
import logging
import requests
from aiohttp.hdrs import CONTENT_TYPE
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_USERNAME, CONF_API_KEY, CONF_RECIPIENT, HTTP_HEADER_CONTENT_TYPE,
CONTENT_TYPE_JSON)
from homeassistant.components.notify import (
PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import (
CONF_API_KEY, CONF_USERNAME, CONF_RECIPIENT, CONTENT_TYPE_JSON)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
BASE_API_URL = 'https://rest.clicksend.com/v3'
HEADERS = {HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_JSON}
HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON}
CONF_LANGUAGE = 'language'
CONF_VOICE = 'voice'
+4 -4
View File
@@ -6,14 +6,14 @@ https://home-assistant.io/components/notify.facebook/
"""
import logging
from aiohttp.hdrs import CONTENT_TYPE
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import (
ATTR_TARGET, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService)
ATTR_DATA, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import CONTENT_TYPE_JSON
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -70,7 +70,7 @@ class FacebookNotificationService(BaseNotificationService):
import json
resp = requests.post(BASE_URL, data=json.dumps(body),
params=payload,
headers={'Content-Type': CONTENT_TYPE_JSON},
headers={CONTENT_TYPE: CONTENT_TYPE_JSON},
timeout=10)
if resp.status_code != 200:
obj = resp.json()
@@ -13,7 +13,7 @@ from homeassistant.components.notify import (
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['freesms==0.1.1']
REQUIREMENTS = ['freesms==0.1.2']
_LOGGER = logging.getLogger(__name__)
+41 -61
View File
@@ -5,27 +5,29 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.html5/
"""
import asyncio
import os
import logging
import json
import time
import datetime
import json
import logging
import time
import uuid
from aiohttp.hdrs import AUTHORIZATION
import voluptuous as vol
from voluptuous.humanize import humanize_error
from homeassistant.const import (HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR,
HTTP_UNAUTHORIZED, URL_ROOT)
from homeassistant.util import ensure_unique_string
from homeassistant.components.notify import (
ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA,
BaseNotificationService, PLATFORM_SCHEMA)
from homeassistant.components.http import HomeAssistantView
from homeassistant.util.json import load_json, save_json
from homeassistant.exceptions import HomeAssistantError
from homeassistant.components.frontend import add_manifest_json_key
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.notify import (
ATTR_DATA, ATTR_TITLE, ATTR_TARGET, PLATFORM_SCHEMA, ATTR_TITLE_DEFAULT,
BaseNotificationService)
from homeassistant.const import (
URL_ROOT, HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED, HTTP_INTERNAL_SERVER_ERROR)
from homeassistant.helpers import config_validation as cv
from homeassistant.util import ensure_unique_string
REQUIREMENTS = ['pywebpush==1.1.0', 'PyJWT==1.5.3']
REQUIREMENTS = ['pywebpush==1.3.0', 'PyJWT==1.5.3']
DEPENDENCIES = ['frontend']
@@ -62,24 +64,25 @@ ATTR_JWT = 'jwt'
# is valid.
JWT_VALID_DAYS = 7
KEYS_SCHEMA = vol.All(dict,
vol.Schema({
vol.Required(ATTR_AUTH): cv.string,
vol.Required(ATTR_P256DH): cv.string
}))
KEYS_SCHEMA = vol.All(
dict, vol.Schema({
vol.Required(ATTR_AUTH): cv.string,
vol.Required(ATTR_P256DH): cv.string,
})
)
SUBSCRIPTION_SCHEMA = vol.All(dict,
vol.Schema({
# pylint: disable=no-value-for-parameter
vol.Required(ATTR_ENDPOINT): vol.Url(),
vol.Required(ATTR_KEYS): KEYS_SCHEMA,
vol.Optional(ATTR_EXPIRATIONTIME):
vol.Any(None, cv.positive_int)
}))
SUBSCRIPTION_SCHEMA = vol.All(
dict, vol.Schema({
# pylint: disable=no-value-for-parameter
vol.Required(ATTR_ENDPOINT): vol.Url(),
vol.Required(ATTR_KEYS): KEYS_SCHEMA,
vol.Optional(ATTR_EXPIRATIONTIME): vol.Any(None, cv.positive_int),
})
)
REGISTER_SCHEMA = vol.Schema({
vol.Required(ATTR_SUBSCRIPTION): SUBSCRIPTION_SCHEMA,
vol.Required(ATTR_BROWSER): vol.In(['chrome', 'firefox'])
vol.Required(ATTR_BROWSER): vol.In(['chrome', 'firefox']),
})
CALLBACK_EVENT_PAYLOAD_SCHEMA = vol.Schema({
@@ -123,21 +126,11 @@ def get_service(hass, config, discovery_info=None):
def _load_config(filename):
"""Load configuration."""
if not os.path.isfile(filename):
return {}
try:
with open(filename, 'r') as fdesc:
inp = fdesc.read()
# In case empty file
if not inp:
return {}
return json.loads(inp)
except (IOError, ValueError) as error:
_LOGGER.error("Reading config file %s failed: %s", filename, error)
return None
return load_json(filename)
except HomeAssistantError:
pass
return {}
class JSONBytesDecoder(json.JSONEncoder):
@@ -145,24 +138,12 @@ class JSONBytesDecoder(json.JSONEncoder):
# pylint: disable=method-hidden
def default(self, obj):
"""Decode object if it's a bytes object, else defer to baseclass."""
"""Decode object if it's a bytes object, else defer to base class."""
if isinstance(obj, bytes):
return obj.decode()
return json.JSONEncoder.default(self, obj)
def _save_config(filename, config):
"""Save configuration."""
try:
with open(filename, 'w') as fdesc:
fdesc.write(json.dumps(
config, cls=JSONBytesDecoder, indent=4, sort_keys=True))
except (IOError, TypeError) as error:
_LOGGER.error("Saving config file failed: %s", error)
return False
return True
class HTML5PushRegistrationView(HomeAssistantView):
"""Accepts push registrations from a browser."""
@@ -192,7 +173,7 @@ class HTML5PushRegistrationView(HomeAssistantView):
self.registrations[name] = data
if not _save_config(self.json_path, self.registrations):
if not save_json(self.json_path, self.registrations):
return self.json_message(
'Error saving registration.', HTTP_INTERNAL_SERVER_ERROR)
@@ -221,7 +202,7 @@ class HTML5PushRegistrationView(HomeAssistantView):
reg = self.registrations.pop(found)
if not _save_config(self.json_path, self.registrations):
if not save_json(self.json_path, self.registrations):
self.registrations[found] = reg
return self.json_message(
'Error saving registration.', HTTP_INTERNAL_SERVER_ERROR)
@@ -266,7 +247,7 @@ class HTML5PushCallbackView(HomeAssistantView):
def check_authorization_header(self, request):
"""Check the authorization header."""
import jwt
auth = request.headers.get('Authorization', None)
auth = request.headers.get(AUTHORIZATION, None)
if not auth:
return self.json_message('Authorization header is expected',
status_code=HTTP_UNAUTHORIZED)
@@ -323,8 +304,7 @@ class HTML5PushCallbackView(HomeAssistantView):
event_name = '{}.{}'.format(NOTIFY_CALLBACK_EVENT,
event_payload[ATTR_TYPE])
request.app['hass'].bus.fire(event_name, event_payload)
return self.json({'status': 'ok',
'event': event_payload[ATTR_TYPE]})
return self.json({'status': 'ok', 'event': event_payload[ATTR_TYPE]})
class HTML5NotificationService(BaseNotificationService):
@@ -410,9 +390,9 @@ class HTML5NotificationService(BaseNotificationService):
if response.status_code == 410:
_LOGGER.info("Notification channel has expired")
reg = self.registrations.pop(target)
if not _save_config(self.registrations_json_path,
self.registrations):
if not save_json(self.registrations_json_path,
self.registrations):
self.registrations[target] = reg
_LOGGER.error("Error saving registration.")
_LOGGER.error("Error saving registration")
else:
_LOGGER.info("Configuration saved")
+5 -5
View File
@@ -7,14 +7,14 @@ https://home-assistant.io/components/notify.instapush/
import json
import logging
from aiohttp.hdrs import CONTENT_TYPE
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import (
ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import (
CONF_API_KEY, HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON)
ATTR_TITLE, PLATFORM_SCHEMA, ATTR_TITLE_DEFAULT, BaseNotificationService)
from homeassistant.const import CONF_API_KEY, CONTENT_TYPE_JSON
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
_RESOURCE = 'https://api.instapush.im/v1/'
@@ -76,7 +76,7 @@ class InstapushNotificationService(BaseNotificationService):
self._headers = {
HTTP_HEADER_APPID: self._api_key,
HTTP_HEADER_APPSECRET: self._app_secret,
HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_JSON,
CONTENT_TYPE: CONTENT_TYPE_JSON,
}
def send_message(self, message="", **kwargs):
+13 -11
View File
@@ -13,9 +13,10 @@ from homeassistant.components.notify import (
from homeassistant.const import CONF_ICON
import homeassistant.helpers.config_validation as cv
from homeassistant.components.lametric import DOMAIN
from homeassistant.components.lametric import DOMAIN as LAMETRIC_DOMAIN
REQUIREMENTS = ['lmnotify==0.0.4']
DEPENDENCIES = ['lametric']
_LOGGER = logging.getLogger(__name__)
@@ -30,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-variable
def get_service(hass, config, discovery_info=None):
"""Get the Slack notification service."""
hlmn = hass.data.get(DOMAIN)
hlmn = hass.data.get(LAMETRIC_DOMAIN)
return LaMetricNotificationService(hlmn,
config[CONF_ICON],
config[CONF_DISPLAY_TIME] * 1000)
@@ -49,6 +50,7 @@ class LaMetricNotificationService(BaseNotificationService):
def send_message(self, message="", **kwargs):
"""Send a message to some LaMetric deviced."""
from lmnotify import SimpleFrame, Sound, Model
from oauthlib.oauth2 import TokenExpiredError
targets = kwargs.get(ATTR_TARGET)
data = kwargs.get(ATTR_DATA)
@@ -76,16 +78,16 @@ class LaMetricNotificationService(BaseNotificationService):
frames = [text_frame]
if sound is not None:
frames.append(sound)
_LOGGER.debug(frames)
model = Model(frames=frames)
lmn = self.hasslametricmanager.manager()
devices = lmn.get_devices()
model = Model(frames=frames, sound=sound)
lmn = self.hasslametricmanager.manager
try:
devices = lmn.get_devices()
except TokenExpiredError:
_LOGGER.debug("Token expired, fetching new token")
lmn.get_token()
devices = lmn.get_devices()
for dev in devices:
if (targets is None) or (dev["name"] in targets):
if targets is None or dev["name"] in targets:
lmn.set_device(dev)
lmn.send_notification(model, lifetime=self._display_time)
_LOGGER.debug("Sent notification to LaMetric %s", dev["name"])
+3 -2
View File
@@ -10,7 +10,8 @@ import voluptuous as vol
from homeassistant.components.notify import (
ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT)
from homeassistant.const import (
CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT, CONTENT_TYPE_TEXT_PLAIN)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['sendgrid==5.3.0']
@@ -67,7 +68,7 @@ class SendgridNotificationService(BaseNotificationService):
},
"content": [
{
"type": "text/plain",
"type": CONTENT_TYPE_TEXT_PLAIN,
"value": message
}
]
@@ -13,7 +13,7 @@ from homeassistant.components.notify import (
ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import CONF_PASSWORD
REQUIREMENTS = ['simplepush==1.1.3']
REQUIREMENTS = ['simplepush==1.1.4']
_LOGGER = logging.getLogger(__name__)
+10 -1
View File
@@ -21,6 +21,7 @@ DEPENDENCIES = [DOMAIN]
ATTR_KEYBOARD = 'keyboard'
ATTR_INLINE_KEYBOARD = 'inline_keyboard'
ATTR_PHOTO = 'photo'
ATTR_VIDEO = 'video'
ATTR_DOCUMENT = 'document'
CONF_CHAT_ID = 'chat_id'
@@ -63,7 +64,7 @@ class TelegramNotificationService(BaseNotificationService):
keys = keys if isinstance(keys, list) else [keys]
service_data.update(inline_keyboard=keys)
# Send a photo, a document or a location
# Send a photo, video, document, or location
if data is not None and ATTR_PHOTO in data:
photos = data.get(ATTR_PHOTO, None)
photos = photos if isinstance(photos, list) else [photos]
@@ -72,6 +73,14 @@ class TelegramNotificationService(BaseNotificationService):
self.hass.services.call(
DOMAIN, 'send_photo', service_data=service_data)
return
elif data is not None and ATTR_VIDEO in data:
videos = data.get(ATTR_VIDEO, None)
videos = videos if isinstance(videos, list) else [videos]
for video_data in videos:
service_data.update(video_data)
self.hass.services.call(
DOMAIN, 'send_video', service_data=service_data)
return
elif data is not None and ATTR_LOCATION in data:
service_data.update(data.get(ATTR_LOCATION))
return self.hass.services.call(
+5 -4
View File
@@ -6,12 +6,13 @@ https://home-assistant.io/components/notify.telstra/
"""
import logging
from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION
import requests
import voluptuous as vol
from homeassistant.components.notify import (
BaseNotificationService, ATTR_TITLE, PLATFORM_SCHEMA)
from homeassistant.const import CONTENT_TYPE_JSON, HTTP_HEADER_CONTENT_TYPE
ATTR_TITLE, PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import CONTENT_TYPE_JSON
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -73,8 +74,8 @@ class TelstraNotificationService(BaseNotificationService):
}
message_resource = 'https://api.telstra.com/v1/sms/messages'
message_headers = {
HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_JSON,
'Authorization': 'Bearer ' + token_response['access_token'],
CONTENT_TYPE: CONTENT_TYPE_JSON,
AUTHORIZATION: 'Bearer {}'.format(token_response['access_token']),
}
message_response = requests.post(
message_resource, headers=message_headers, json=message_data,
+5 -2
View File
@@ -9,6 +9,7 @@ import time
import requests
import voluptuous as vol
from aiohttp.hdrs import CONTENT_TYPE
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONTENT_TYPE_JSON
import homeassistant.helpers.config_validation as cv
@@ -55,8 +56,10 @@ class OctoPrintAPI(object):
def __init__(self, api_url, key, bed, number_of_tools):
"""Initialize OctoPrint API and set headers needed later."""
self.api_url = api_url
self.headers = {'content-type': CONTENT_TYPE_JSON,
'X-Api-Key': key}
self.headers = {
CONTENT_TYPE: CONTENT_TYPE_JSON,
'X-Api-Key': key,
}
self.printer_last_reading = [{}, None]
self.job_last_reading = [{}, None]
self.job_available = False

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